Files
Felix KemmlerandClaude Sonnet 5 b6c1389c55 Reorganize into monorepo layout, move mitarbeiter-app to legacy reference
Consolidates the previously separate omsorgapp and omsorgCore repos
(each had their own nested .git with GitHub history) plus the old
root-level website/mitarbeiter-app into a single monorepo, matching
the structure already documented in the root CLAUDE.md. Also moves
the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to
serve as a template for a ground-up rewrite.

Fixes .gitignore in the same pass: the config-secrets/uploads/data
patterns were unanchored (relative to repo root, not depth-agnostic),
so they silently stopped matching once the app moved under omsorgWeb/.
Patterns are now **/-prefixed and cover both mitarbeiter-app and
mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded
employee documents out of version control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 14:21:37 +02:00

133 lines
4.7 KiB
PHP

<?php
require_once __DIR__ . '/auth.php';
// Gemeinsame Endung→MIME-Whitelists.
const UPLOAD_TYPES_DOCS = [
'pdf' => ['application/pdf'],
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
];
const UPLOAD_TYPES_PDF_IMG = [
'pdf' => ['application/pdf'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
];
const UPLOAD_TYPES_IMAGES = [
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'gif' => ['image/gif'],
'webp' => ['image/webp'],
];
const UPLOAD_TYPES_PDF = [
'pdf' => ['application/pdf'],
];
const UPLOAD_TYPES_OFFICE = [
'pdf' => ['application/pdf'],
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'xls' => ['application/vnd.ms-excel'],
'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
'ppt' => ['application/vnd.ms-powerpoint'],
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
'txt' => ['text/plain'],
'zip' => ['application/zip', 'application/x-zip-compressed'],
];
/**
* Validiert und speichert eine hochgeladene Datei.
*
* @param array $file Eintrag aus $_FILES, z.B. $_FILES['datei']
* @param array $opts allowed (ext=>mimes, Pflicht), dir (Pflicht), prefix,
* max_bytes (Default 12 MB), required (Default true),
* naming ('dated' [Default] = Datum_Username_Prefix_Rand,
* 'plain' = Username_Rand, 'hash' = nur Rand),
* username (Default current_username(); für 'dated'/'plain'),
* messages (Überschreibt Standardtexte: missing/upload/ext/mime/size/move)
* @return array ['ok'=>bool, 'error'=>?string, 'filename'=>?string,
* 'original_name'=>?string, 'mime'=>?string]
*/
function handle_upload(array $file, array $opts): array {
$allowed = $opts['allowed'];
$dir = rtrim($opts['dir'], '/') . '/';
$maxBytes = $opts['max_bytes'] ?? 12 * 1024 * 1024;
$required = $opts['required'] ?? true;
$naming = $opts['naming'] ?? 'dated';
$prefix = $opts['prefix'] ?? 'datei';
$username = $opts['username'] ?? current_username();
$msg = array_merge([
'missing' => 'Bitte eine Datei auswählen.',
'upload' => 'Fehler beim Hochladen der Datei.',
'ext' => 'Dateityp nicht erlaubt.',
'mime' => 'Dateityp nicht erlaubt.',
'size' => 'Datei ist zu groß (max. 12 MB).',
'move' => 'Fehler beim Hochladen der Datei.',
], $opts['messages'] ?? []);
$err = $file['error'] ?? UPLOAD_ERR_NO_FILE;
if ($err === UPLOAD_ERR_NO_FILE || !isset($file['tmp_name'])) {
if ($required) return ['ok' => false, 'error' => $msg['missing']];
return ['ok' => true, 'error' => null, 'filename' => null, 'original_name' => null, 'mime' => null];
}
if ($err !== UPLOAD_ERR_OK) {
return ['ok' => false, 'error' => $msg['upload']];
}
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!isset($allowed[$ext])) {
return ['ok' => false, 'error' => $msg['ext']];
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowed[$ext], true)) {
return ['ok' => false, 'error' => $msg['mime']];
}
if ($file['size'] > $maxBytes) {
return ['ok' => false, 'error' => $msg['size']];
}
$safeName = match ($naming) {
'hash' => bin2hex(random_bytes(16)) . '.' . $ext,
'plain' => $username . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
default => date('Y-m-d') . '_' . $username . '_' . $prefix . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
};
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
if (!move_uploaded_file($file['tmp_name'], $dir . $safeName)) {
return ['ok' => false, 'error' => $msg['move']];
}
return [
'ok' => true,
'error' => null,
'filename' => $safeName,
'original_name' => $originalName,
'mime' => $mime,
];
}
function redirect_error(string $back, string $msg): never {
header('Location: ' . $back . '?error=' . urlencode($msg));
exit;
}
function redirect_ok(string $back, string $param = 'success', string $val = '1'): never {
header('Location: ' . $back . '?' . $param . '=' . $val);
exit;
}