Files
2026-10-21 22:00:31 +02:00

249 lines
12 KiB
PHP

<?php
/**
* admin/import_ntm.php
*
* Interface d'upload manuel d'un fichier .ntm directement depuis l'admin
* (en plus du flux existant via contribute.php). Reutilise
* importNtmFileToStaging() de lib/ntm_import_to_staging.php : le fichier
* est parse, chaque cellule extraite atterrit dans staging_cells (status
* pending), rien n'est ecrit dans sites/cells sans validation manuelle
* via /admin/review.php.
*
* Apres import, la page affiche un APERCU du regroupement par site
* (meme logique deriveSiteKey() que review.php) pour confirmer
* immediatement que les cellules d'un meme site sont bien reunies
* ensemble, avant meme d'aller sur la page de validation.
*
* Tous les fichiers numeriques passes a htmlspecialchars() sont castes
* en (string) au prealable (voir correctifs precedents sur review.php
* et edit_site.php pour PHP 8.3+).
*
* Corrige suite a l'audit du 20/09/2026 : le detail technique d'une
* exception ($e->getMessage()) n'est plus affiche a l'ecran ; il est
* journalise via error_log() et un message generique est montre a
* l'administrateur.
*/
declare(strict_types=1);
require_once __DIR__ . '/admin_auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/ntm_import_to_staging.php';
require_once __DIR__ . '/_nav.php';
require_admin_auth();
$db = pdo();
$user = current_admin_username();
$successMessage = null;
$errorMessage = null;
$importStats = null;
$previewGroups = [];
$lastImportId = null;
/**
* Meme logique de regroupement que admin/review.php, dupliquee ici
* volontairement pour ne pas creer de dependance croisee entre pages
* admin. Si vous ajustez le decalage de bits ou la logique de
* regroupement dans review.php, reportez le changement ici aussi.
*/
function deriveSiteKeyForPreview(array $row): string
{
$mcc = $row['mcc'];
$mnc = $row['mnc'];
if ($row['tech'] === '4G' && !empty($row['sub_id2'])) {
return "enb:{$mcc}:{$mnc}:{$row['area_code']}:{$row['sub_id2']}";
}
$latR = $row['lat'] !== null ? round((float)$row['lat'], 4) : 'na';
$lonR = $row['lon'] !== null ? round((float)$row['lon'], 4) : 'na';
return "geo:{$mcc}:{$mnc}:{$row['area_code']}:{$latR}:{$lonR}";
}
const MAX_UPLOAD_SIZE_BYTES = 10 * 1024 * 1024; // 10 Mo, garde-fou raisonnable pour un fichier .ntm
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf_token();
$countryId = (int)($_POST['country_id'] ?? 0);
if (!$countryId) {
$errorMessage = "Selectionnez un pays avant d'importer.";
} elseif (empty($_FILES['ntm_file']) || $_FILES['ntm_file']['error'] !== UPLOAD_ERR_OK) {
$uploadErrors = [
UPLOAD_ERR_INI_SIZE => "Le fichier depasse la taille maximale autorisee par le serveur (php.ini).",
UPLOAD_ERR_FORM_SIZE => "Le fichier depasse la taille maximale autorisee par le formulaire.",
UPLOAD_ERR_PARTIAL => "Le fichier n'a ete que partiellement televerse. Reessayez.",
UPLOAD_ERR_NO_FILE => "Aucun fichier selectionne.",
];
$code = $_FILES['ntm_file']['error'] ?? UPLOAD_ERR_NO_FILE;
$errorMessage = $uploadErrors[$code] ?? "Erreur lors du televersement du fichier.";
} else {
$tmpPath = $_FILES['ntm_file']['tmp_name'];
$originalName = $_FILES['ntm_file']['name'];
$fileSize = $_FILES['ntm_file']['size'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($extension !== 'ntm') {
$errorMessage = "Seuls les fichiers .ntm sont acceptes (extension detectee : ." . htmlspecialchars($extension, ENT_QUOTES, 'UTF-8') . ").";
} elseif ($fileSize > MAX_UPLOAD_SIZE_BYTES) {
$errorMessage = "Le fichier depasse la limite de " . (MAX_UPLOAD_SIZE_BYTES / 1024 / 1024) . " Mo.";
} elseif (!is_uploaded_file($tmpPath)) {
$errorMessage = "Televersement invalide (fichier non reconnu comme upload legitime).";
} else {
$safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', $originalName);
$uploadsDir = __DIR__ . '/../uploads';
if (!is_dir($uploadsDir)) {
mkdir($uploadsDir, 0750, true);
}
$destPath = $uploadsDir . '/' . uniqid('ntm_', true) . '_' . $safeFilename;
if (!move_uploaded_file($tmpPath, $destPath)) {
$errorMessage = "Impossible d'enregistrer le fichier televerse sur le serveur.";
} else {
try {
$importStats = importNtmFileToStaging($destPath, $safeFilename, $countryId);
$successMessage = "Import termine : {$importStats['staged']} ligne(s) placee(s) en validation"
. ($importStats['already_known'] > 0 ? ", dont {$importStats['already_known']} deja connue(s) en base" : "")
. ($importStats['error'] > 0 ? ", {$importStats['error']} erreur(s)" : "") . ".";
// Recupere l'ID de l'import qui vient d'etre cree pour l'apercu de regroupement
$lastImportId = (int)$db->query('SELECT MAX(id) FROM imports')->fetchColumn();
$previewStmt = $db->prepare('
SELECT sc.*, o.brand AS operator_brand
FROM staging_cells sc
LEFT JOIN operators o ON o.id = sc.operator_guess
WHERE sc.import_id = ?
');
$previewStmt->execute([$lastImportId]);
$importedRows = $previewStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($importedRows as $row) {
$key = deriveSiteKeyForPreview($row);
$previewGroups[$key]['cells'][] = $row;
$previewGroups[$key]['operator_brand'] = $row['operator_brand'];
$previewGroups[$key]['lat'] = $previewGroups[$key]['lat'] ?? $row['lat'];
$previewGroups[$key]['lon'] = $previewGroups[$key]['lon'] ?? $row['lon'];
}
} catch (Throwable $e) {
error_log('[import_ntm] ' . $e->getMessage());
$errorMessage = "Une erreur est survenue lors du traitement du fichier. Consultez les journaux du serveur.";
}
}
}
}
}
$countries = $db->query('SELECT id, iso2, name FROM countries ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
$importHistory = $db->query('
SELECT i.*, c.name AS country_name
FROM imports i
LEFT JOIN countries c ON c.id = i.country_id
ORDER BY i.uploaded_at DESC
LIMIT 20
')->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"><title>NetMonster Admin - Import .ntm</title>
<link rel="stylesheet" href="assets/theme.css">
<style>
.upload-zone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 1.5rem; text-align: center; background: var(--surface-alt); }
.upload-zone input[type="file"] { margin-top: .8rem; }
.stats-inline { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; }
.preview-group { background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .8rem 1rem; margin-bottom: .7rem; }
.preview-group h4 { margin: 0 0 .4rem; font-size: .88rem; }
.preview-group .cell-line { font-size: .78rem; color: var(--text-muted); font-family: monospace; }
</style>
</head>
<body>
<?php render_admin_nav('import_ntm.php', $user); ?>
<div class="page-wrap">
<h1>Importer un fichier .ntm</h1>
<p class="page-subtitle">Deposez un fichier .ntm pour l'analyser et placer ses antennes en file de validation. Rien n'est ecrit dans la base finale sans validation manuelle.</p>
<?php if ($successMessage): ?><p class="msg-ok"><?= htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8') ?></p><?php endif; ?>
<?php if ($errorMessage): ?><p class="msg-err"><?= htmlspecialchars($errorMessage, ENT_QUOTES, 'UTF-8') ?></p><?php endif; ?>
<div class="card">
<h2>Nouveau depot</h2>
<form method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="form-grid" style="margin-bottom:1rem;">
<div>
<label>Pays concerne</label>
<select name="country_id" required>
<option value="">-- choisir --</option>
<?php foreach ($countries as $c): ?>
<option value="<?= (int)$c['id'] ?>"><?= htmlspecialchars($c['name'], ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="upload-zone">
<p>Selectionnez un fichier <strong>.ntm</strong> (taille maximale : 10 Mo)</p>
<input type="file" name="ntm_file" accept=".ntm" required>
</div>
<button type="submit" class="primary" style="margin-top:1rem;">Importer et envoyer en validation</button>
</form>
</div>
<?php if ($importStats): ?>
<div class="card">
<h2>Resultat de l'import</h2>
<div class="stats-inline">
<div class="stat-chip"><strong><?= (int)$importStats['total'] ?></strong>ligne(s) lues</div>
<div class="stat-chip"><strong><?= (int)$importStats['staged'] ?></strong>placee(s) en validation</div>
<div class="stat-chip"><strong><?= (int)$importStats['already_known'] ?></strong>deja connue(s) (doublon potentiel)</div>
<div class="stat-chip"><strong><?= (int)$importStats['error'] ?></strong>erreur(s)</div>
</div>
<h2 style="margin-top:1.4rem;">Apercu du regroupement par site (<?= count($previewGroups) ?> site(s) detecte(s))</h2>
<p class="page-subtitle">Verification rapide que les cellules d'un meme site (meme eNB pour la 4G, ou meme position GPS pour les autres technos) sont bien reunies ensemble.</p>
<?php foreach ($previewGroups as $group): ?>
<div class="preview-group">
<h4><?= htmlspecialchars($group['operator_brand'] ?? 'Operateur inconnu', ENT_QUOTES, 'UTF-8') ?> &middot; <?= htmlspecialchars((string)$group['lat'], ENT_QUOTES, 'UTF-8') ?>, <?= htmlspecialchars((string)$group['lon'], ENT_QUOTES, 'UTF-8') ?> &middot; <?= count($group['cells']) ?> cellule<?= count($group['cells']) > 1 ? 's' : '' ?></h4>
<?php foreach ($group['cells'] as $c): ?>
<div class="cell-line">
<?= htmlspecialchars((string)$c['tech'], ENT_QUOTES, 'UTF-8') ?> &middot;
Cell ID <?= htmlspecialchars((string)$c['cell_id'], ENT_QUOTES, 'UTF-8') ?> &middot;
Area code <?= htmlspecialchars((string)$c['area_code'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!empty($c['sub_id2'])): ?> &middot; eNB/RNC <?= htmlspecialchars((string)$c['sub_id2'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
<?php if (!empty($c['band'])): ?> &middot; <?= htmlspecialchars((string)$c['band'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
<?php if ($c['possible_dup_of']): ?> &middot; <span class="badge badge-dup">Deja en base #<?= (int)$c['possible_dup_of'] ?></span><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<p style="margin-top:1rem;"><a href="review.php" class="btn primary" style="text-decoration:none;display:inline-block;">Aller a la page de validation &rarr;</a></p>
</div>
<?php endif; ?>
<div class="card">
<h2>Historique des imports</h2>
<table class="data-table">
<tr><th>Fichier</th><th>Pays</th><th>Date</th><th>Lignes</th><th>En validation</th><th>Erreurs</th><th>Statut</th></tr>
<?php foreach ($importHistory as $imp): ?>
<tr>
<td><?= htmlspecialchars($imp['filename'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($imp['country_name'] ?? '?', ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars(date('d/m/Y H:i', strtotime($imp['uploaded_at'])), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= (int)$imp['total_lines'] ?></td>
<td><?= (int)$imp['staged_count'] ?></td>
<td><?= (int)$imp['error_count'] ?></td>
<td><?= htmlspecialchars($imp['status'], ENT_QUOTES, 'UTF-8') ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($importHistory)): ?><tr><td colspan="7" class="no-cadastre">Aucun import pour le moment.</td></tr><?php endif; ?>
</table>
</div>
</div>
</body>
</html>