1e51ef7def
- 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>
134 lines
5.0 KiB
PHP
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),
|
|
]);
|