Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

134 lines
5.0 KiB
PHP

<?php
/**
* POST /api/teacher/students-csv multipart/form-data
* classId — Pflicht
* file — CSV-Datei (UTF-8), Komma- oder Semikolon-Trenner
*
* Erwartetes CSV-Format (mit Header-Zeile):
* username,password,display_name,first_name,last_name,avatar_slug
*
* Mindest-Pflicht pro Zeile: username + password.
* Andere Felder optional. Default-Avatar: fuchs-explorer.
*
* Response: { created: N, skipped: [{username, reason}], errors: [...] }
*/
declare(strict_types=1);
require_once __DIR__ . '/../../bootstrap.php';
v2_validate_method('POST');
$auth = v2_auth_require('teacher');
$teacherId = (int) ($auth['sub'] ?? 0);
$classId = (int) ($_POST['classId'] ?? 0);
if (!$classId) v2_response_error(400, 'missing_classId', 'classId fehlt.');
$own = v2_db_one('SELECT id FROM classes WHERE id = :c AND teacher_id = :t',
[':c' => $classId, ':t' => $teacherId]);
if (!$own) v2_response_error(403, 'forbidden', 'Klasse nicht deine.');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
v2_response_error(400, 'upload_failed', 'CSV-Datei fehlt oder Upload-Fehler.');
}
if ($_FILES['file']['size'] > 1024 * 1024) {
v2_response_error(413, 'file_too_large', 'CSV max. 1 MB.');
}
$content = file_get_contents($_FILES['file']['tmp_name']);
// BOM entfernen, Zeilenenden normalisieren
$content = preg_replace('/^\xEF\xBB\xBF/', '', $content);
$content = str_replace(["\r\n", "\r"], "\n", $content);
// Trenner auto-erkennen aus erster Zeile
$firstLine = strtok($content, "\n");
$delimiter = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
$lines = array_filter(array_map('trim', explode("\n", $content)), fn($l) => $l !== '');
if (count($lines) < 2) {
v2_response_error(400, 'csv_empty', 'CSV muss Header und mindestens eine Datenzeile haben.');
}
$header = str_getcsv(array_shift($lines), $delimiter);
$header = array_map(fn($h) => strtolower(trim($h)), $header);
$colIdx = function (string $key) use ($header) {
$i = array_search($key, $header, true);
return $i === false ? null : $i;
};
$idx = [
'username' => $colIdx('username'),
'password' => $colIdx('password'),
'display_name' => $colIdx('display_name') ?? $colIdx('displayname'),
'first_name' => $colIdx('first_name') ?? $colIdx('firstname') ?? $colIdx('vorname'),
'last_name' => $colIdx('last_name') ?? $colIdx('lastname') ?? $colIdx('nachname'),
'avatar_slug' => $colIdx('avatar_slug') ?? $colIdx('avatarslug') ?? $colIdx('avatar'),
];
if ($idx['username'] === null || $idx['password'] === null) {
v2_response_error(400, 'csv_invalid_header',
'CSV-Header muss mindestens "username" und "password" enthalten.', ['headerGefunden' => $header]);
}
$pdo = v2_db();
$pdo->beginTransaction();
$created = 0;
$skipped = [];
$lineNo = 1;
try {
$stmtCheck = $pdo->prepare(
'SELECT id FROM students WHERE class_id = :c AND username = :u AND deleted_at IS NULL'
);
$stmtInsert = $pdo->prepare(
'INSERT INTO students
(class_id, username, password, display_name, first_name, last_name, avatar_slug, is_anonymous)
VALUES (:cid, :u, :p, :dn, :fn, :ln, :av, 0)'
);
foreach ($lines as $line) {
$lineNo++;
$row = str_getcsv($line, $delimiter);
$username = trim((string) ($row[$idx['username']] ?? ''));
$password = (string) ($row[$idx['password']] ?? '');
if (!$username || !$password) {
$skipped[] = ['line' => $lineNo, 'username' => $username, 'reason' => 'username oder password fehlt'];
continue;
}
if (strlen($password) < 4) {
$skipped[] = ['line' => $lineNo, 'username' => $username, 'reason' => 'Passwort < 4 Zeichen'];
continue;
}
$stmtCheck->execute([':c' => $classId, ':u' => $username]);
if ($stmtCheck->fetch()) {
$skipped[] = ['line' => $lineNo, 'username' => $username, 'reason' => 'Username schon vergeben in dieser Klasse'];
continue;
}
$displayName = trim((string) ($row[$idx['display_name']] ?? '')) ?: $username;
$firstName = $idx['first_name'] !== null ? (trim((string) ($row[$idx['first_name']] ?? '')) ?: null) : null;
$lastName = $idx['last_name'] !== null ? (trim((string) ($row[$idx['last_name']] ?? '')) ?: null) : null;
$avatarSlug = $idx['avatar_slug'] !== null ? (trim((string) ($row[$idx['avatar_slug']] ?? '')) ?: 'fuchs-explorer')
: 'fuchs-explorer';
$stmtInsert->execute([
':cid' => $classId, ':u' => $username,
':p' => password_hash($password, PASSWORD_DEFAULT),
':dn' => $displayName, ':fn' => $firstName, ':ln' => $lastName, ':av' => $avatarSlug,
]);
$created++;
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
v2_response_ok([
'created' => $created,
'skipped' => $skipped,
'total' => count($lines),
]);