Files
Arnaud 5633969d98 Version initiale — carte des établissements scolaires
Carte Leaflet (annuaire Éducation nationale) + API PHP/MySQL :
fiche établissement (effectifs, IPS, population INSEE, DPE ADEME),
GED documents/photos, commentaires par groupe, gestion utilisateurs
et politique de mots de passe. Imports opendata (effectifs, IPS,
population, DPE, IRIS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:35:41 +02:00

274 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Éléments d'un établissement : fichiers, photos, liens.
*
* GET ?uai=UAI [&kind=] → éléments VALIDÉS (public)
* GET ?action=queue → file d'attente de modération (admin, selon périmètre)
* POST ?uai=UAI (multipart) → soumission par tout internaute (statut « pending »)
* champs : kind, folder_id?, title?, description?, url?,
* submitter_name?, submitter_email?
* fichier : « file » (pour kind=file|photo)
* POST ?action=validate (JSON) → {id, decision:'approve'|'reject', reason?} (admin)
* POST ?action=update (JSON) → {id, title, description?} édition (valideurs)
* DELETE ?id=N → suppression (admin, selon périmètre)
*/
declare(strict_types=1);
require __DIR__ . '/db.php';
/** Sous-dossier d'upload selon le type. */
function kind_dir(string $kind): string
{
return $kind === 'photo' ? 'photos' : 'files';
}
/** URL publique d'un élément stocké sur disque. */
function item_url(array $r): ?string
{
if ($r['kind'] === 'link') {
return $r['url'];
}
if (!$r['filename']) {
return null;
}
return 'uploads/' . kind_dir($r['kind']) . '/' . rawurlencode($r['filename']);
}
function shape_item(array $r): array
{
return [
'id' => (int) $r['id'],
'folder_id' => $r['folder_id'] !== null ? (int) $r['folder_id'] : null,
'kind' => $r['kind'],
'title' => $r['title'],
'description' => $r['description'] ?? null,
'url' => item_url($r),
'size' => $r['size'] !== null ? (int) $r['size'] : null,
'mime' => $r['mime'],
'status' => $r['status'],
'submitted_at' => $r['submitted_at'] ?? null,
];
}
try {
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
// ---- File d'attente de modération (admin) -----------------------------
if ($method === 'GET' && $action === 'queue') {
$user = require_login();
if ($user['role'] !== 'super' && $user['role'] !== 'perimeter') {
json_out([]); // commentateur : aucune file de modération
}
$sql = "SELECT i.*, e.nom AS etab_nom, e.com AS etab_com, e.depc AS etab_depc, e.dep AS etab_dep
FROM items i
JOIN etablissements e ON e.id = i.uai";
$args = [];
if ($user['role'] === 'super') {
$sql .= " WHERE i.status = 'pending'";
} else {
// Périmètre personnel (legacy) OU groupe « valideur » de l'utilisateur.
$sql .= " WHERE i.status = 'pending' AND (
EXISTS (SELECT 1 FROM user_scopes s
WHERE s.user_id = ? AND s.com = e.com AND s.depc = e.depc)
OR EXISTS (SELECT 1 FROM group_members gm
JOIN user_groups g ON g.id = gm.group_id AND g.can_validate = 1
JOIN group_scopes gs ON gs.group_id = g.id AND gs.com = e.com AND gs.depc = e.depc
WHERE gm.user_id = ?))";
$args[] = $user['id'];
$args[] = $user['id'];
}
$sql .= ' ORDER BY i.submitted_at ASC';
$st = db()->prepare($sql);
$st->execute($args);
$out = [];
foreach ($st as $r) {
$out[] = shape_item($r) + [
'uai' => $r['uai'],
'etab' => $r['etab_nom'] . ' — ' . $r['etab_com'] . ' (' . $r['etab_depc'] . ')',
'submitter_name' => $r['submitter_name'],
'submitter_email' => $r['submitter_email'],
];
}
json_out($out);
}
// ---- Liste publique (éléments validés) --------------------------------
if ($method === 'GET') {
$uai = trim((string) ($_GET['uai'] ?? ''));
if ($uai === '') {
json_error('Paramètre uai requis.');
}
$sql = "SELECT * FROM items WHERE uai = ? AND status = 'approved'";
$args = [$uai];
if (!empty($_GET['kind'])) {
$sql .= ' AND kind = ?';
$args[] = $_GET['kind'];
}
$sql .= ' ORDER BY submitted_at DESC, id DESC';
$st = db()->prepare($sql);
$st->execute($args);
$out = [];
foreach ($st as $r) {
$out[] = shape_item($r);
}
json_out($out);
}
// ---- Validation / rejet (admin) ---------------------------------------
if ($method === 'POST' && $action === 'validate') {
$user = require_login();
$b = body_json();
$id = (int) ($b['id'] ?? 0);
$decision = $b['decision'] ?? '';
if ($id <= 0 || !in_array($decision, ['approve', 'reject'], true)) {
json_error('Paramètres invalides (id, decision).');
}
$st = db()->prepare('SELECT uai FROM items WHERE id = ?');
$st->execute([$id]);
$uai = $st->fetchColumn();
if ($uai === false) {
json_error('Élément introuvable.', 404);
}
if (!can_validate_uai($user, (string) $uai)) {
json_error('Hors de votre périmètre.', 403);
}
$status = $decision === 'approve' ? 'approved' : 'rejected';
$reason = $decision === 'reject' ? (trim((string) ($b['reason'] ?? '')) ?: null) : null;
db()->prepare(
'UPDATE items SET status = ?, reject_reason = ?, validated_by = ?, validated_at = NOW() WHERE id = ?'
)->execute([$status, $reason, $user['id'], $id]);
json_out(['ok' => true, 'id' => $id, 'status' => $status]);
}
// ---- Édition du libellé / description (valideurs) ---------------------
if ($method === 'POST' && $action === 'update') {
$user = require_login();
$b = body_json();
$id = (int) ($b['id'] ?? 0);
if ($id <= 0) {
json_error('id requis.');
}
$st = db()->prepare('SELECT uai FROM items WHERE id = ?');
$st->execute([$id]);
$uai = $st->fetchColumn();
if ($uai === false) {
json_error('Élément introuvable.', 404);
}
if (!can_validate_uai($user, (string) $uai)) {
json_error('Hors de votre périmètre.', 403);
}
$title = trim((string) ($b['title'] ?? ''));
$description = trim((string) ($b['description'] ?? ''));
if ($title === '') {
json_error('Le libellé ne peut pas être vide.');
}
db()->prepare('UPDATE items SET title = ?, description = ? WHERE id = ?')
->execute([$title, $description !== '' ? $description : null, $id]);
json_out(['ok' => true, 'id' => $id]);
}
// ---- Soumission (tout internaute) -------------------------------------
if ($method === 'POST') {
$uai = trim((string) ($_GET['uai'] ?? $_POST['uai'] ?? ''));
if ($uai === '') {
json_error('Paramètre uai requis.');
}
$chk = db()->prepare('SELECT 1 FROM etablissements WHERE id = ?');
$chk->execute([$uai]);
if (!$chk->fetchColumn()) {
json_error('Établissement inconnu.', 404);
}
$kind = $_POST['kind'] ?? '';
if (!in_array($kind, ['file', 'photo', 'link'], true)) {
json_error('Type invalide (file, photo ou link).');
}
$folderId = (isset($_POST['folder_id']) && $_POST['folder_id'] !== '')
? (int) $_POST['folder_id'] : null;
if ($folderId !== null) {
$f = db()->prepare('SELECT 1 FROM folders WHERE id = ?');
$f->execute([$folderId]);
if (!$f->fetchColumn()) {
json_error('Dossier inconnu.');
}
}
$title = trim((string) ($_POST['title'] ?? ''));
$description = trim((string) ($_POST['description'] ?? '')) ?: null;
$submitterName = trim((string) ($_POST['submitter_name'] ?? '')) ?: null;
$submitterMail = trim((string) ($_POST['submitter_email'] ?? '')) ?: null;
$filename = null;
$url = null;
$size = null;
$mime = null;
if ($kind === 'link') {
$url = trim((string) ($_POST['url'] ?? ''));
if (!preg_match('#^https?://#i', $url)) {
json_error('URL invalide (doit commencer par http:// ou https://).');
}
if ($title === '') {
$title = $url;
}
} else {
if (empty($_FILES['file']) || (int) $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
json_error('Fichier requis.');
}
$tmp = $_FILES['file']['tmp_name'];
$orig = (string) $_FILES['file']['name'];
$size = (int) $_FILES['file']['size'];
$mime = mime_content_type($tmp) ?: ($_FILES['file']['type'] ?? 'application/octet-stream');
if ($kind === 'photo' && strncmp($mime, 'image/', 6) !== 0) {
json_error('Le fichier nest pas une image.');
}
$dir = dirname(__DIR__) . '/uploads/' . kind_dir($kind);
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$ext = strtolower(preg_replace('/[^A-Za-z0-9]/', '', pathinfo($orig, PATHINFO_EXTENSION) ?: ''));
$filename = bin2hex(random_bytes(8)) . ($ext !== '' ? '.' . $ext : '');
if (!move_uploaded_file($tmp, $dir . '/' . $filename)) {
json_error('Échec de lenregistrement du fichier.', 500);
}
if ($title === '') {
$title = $orig;
}
}
db()->prepare(
'INSERT INTO items (uai, folder_id, kind, title, description, filename, url, size, mime, status, submitter_name, submitter_email)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, \'pending\', ?, ?)'
)->execute([$uai, $folderId, $kind, $title, $description, $filename, $url, $size, $mime, $submitterName, $submitterMail]);
json_out(['ok' => true, 'id' => (int) db()->lastInsertId(), 'status' => 'pending'], 201);
}
// ---- Suppression (admin) ----------------------------------------------
if ($method === 'DELETE') {
$user = require_login();
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) {
json_error('id requis.');
}
$st = db()->prepare('SELECT uai, kind, filename FROM items WHERE id = ?');
$st->execute([$id]);
$row = $st->fetch();
if (!$row) {
json_error('Élément introuvable.', 404);
}
if (!can_validate_uai($user, (string) $row['uai'])) {
json_error('Hors de votre périmètre.', 403);
}
db()->prepare('DELETE FROM comments WHERE item_id = ?')->execute([$id]);
db()->prepare('DELETE FROM items WHERE id = ?')->execute([$id]);
if ($row['filename']) {
@unlink(dirname(__DIR__) . '/uploads/' . kind_dir($row['kind']) . '/' . $row['filename']);
}
json_out(['deleted' => $id]);
}
json_error('Méthode non supportée.', 405);
} catch (Throwable $e) {
json_error('Erreur : ' . $e->getMessage(), 500);
}