5633969d98
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>
274 lines
11 KiB
PHP
274 lines
11 KiB
PHP
<?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 n’est 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 l’enregistrement 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);
|
||
}
|