Fix data export/import: PHP's default upload limits rejected real exports

Root cause: the export embeds the whole gallery as base64 directly in the
JSON (that's how it stays a single self-contained file), which pushes a
realistic export well past 40MB. PHP's post_max_size/upload_max_filesize
default to values well under that on most hosting, so the browser's
upload got silently discarded before the app ever saw it - PHP clears
$_FILES and $_POST once post_max_size is exceeded, and the leftover raw
body in php://input is unparsed multipart data, not JSON, so it fell
into a generic "invalid file" 422 with no indication of what actually
went wrong. Reproduced locally with a real ~43MB export against the
previous 40M limit.

Raises the limits to 200M via two paths, since we don't know which PHP
SAPI the various hosting targets (All-Inkl, the Plesk test server) use:
hifi/.htaccess sets php_value overrides for classic Apache module PHP,
guarded by <IfModule> checks for several common module names so hosts
running PHP-FPM/CGI (which ignore php_value and would otherwise choke on
an unrecognized directive) skip the block instead of 500ing the entire
site; hifi/api/public/.user.ini covers exactly that FPM/CGI case, which
mod_php hosts in turn simply don't read.

Also makes SettingsController::importData() detect an oversized upload
by comparing Content-Length against the configured post_max_size, and
report the actual limit instead of the generic corrupt-file message -
so if some host's real limit is still too low, the admin sees why
instead of a dead end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-21 00:54:34 +02:00
parent b1b66f1813
commit a9aea99028
3 changed files with 79 additions and 1 deletions

View file

@ -22,6 +22,34 @@ RewriteBase /
# Upload mit "Methode nicht erlaubt" fehlschlägt.
DirectorySlash Off
# Groessere Upload-/POST-Limits fuer den Daten-Export/-Import im Admin-Bereich
# (der Export bettet die Bildergalerie als Base64 direkt in die JSON-Datei ein,
# das kann schnell 40+ MB werden - PHPs Standard-Limits liegen oft niedriger).
# Nur relevant, wenn PHP als Apache-Modul laeuft (mod_php) - die IfModule-Pruefung
# sorgt dafuer, dass dieser Block auf FastCGI/PHP-FPM-Hosting (dort greift
# stattdessen api/public/.user.ini) folgenlos uebersprungen wird, statt einen
# "Invalid command"-Fehler fuer die ganze Seite auszuloesen.
<IfModule mod_php.c>
php_value upload_max_filesize 200M
php_value post_max_size 200M
php_value memory_limit 512M
</IfModule>
<IfModule php_module>
php_value upload_max_filesize 200M
php_value post_max_size 200M
php_value memory_limit 512M
</IfModule>
<IfModule php7_module>
php_value upload_max_filesize 200M
php_value post_max_size 200M
php_value memory_limit 512M
</IfModule>
<IfModule php8_module>
php_value upload_max_filesize 200M
php_value post_max_size 200M
php_value memory_limit 512M
</IfModule>
# API-Requests an den PHP-Front-Controller weiterleiten
RewriteCond %{REQUEST_URI} ^/api/
RewriteCond %{REQUEST_FILENAME} !-f

View file

@ -0,0 +1,8 @@
; Groessere Upload-/POST-Limits fuer den Daten-Export/-Import im Admin-Bereich
; (der Export bettet die Bildergalerie als Base64 direkt in die JSON-Datei ein,
; das kann schnell 40+ MB werden). Greift auf PHP-FPM/CGI-Hosting (z.B. viele
; Shared-Hosting-Umgebungen) - fuer klassisches Apache-Modul-PHP (mod_php)
; uebernimmt stattdessen der php_value-Block in hifi/.htaccess.
upload_max_filesize = 200M
post_max_size = 200M
memory_limit = 512M

View file

@ -70,11 +70,53 @@ class SettingsController
exit;
}
/** Wandelt eine php.ini-Groessenangabe ("200M", "1G", "512K") in Bytes um. */
private static function iniBytes(string $value): int
{
$value = trim($value);
if ($value === '') {
return 0;
}
$num = (float) $value;
return match (strtolower(substr($value, -1))) {
'g' => (int) ($num * 1024 * 1024 * 1024),
'm' => (int) ($num * 1024 * 1024),
'k' => (int) ($num * 1024),
default => (int) $value,
};
}
public static function importData(): void
{
if (!empty($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
if (!empty($_FILES['file'])) {
$uploadError = $_FILES['file']['error'];
if ($uploadError === UPLOAD_ERR_INI_SIZE || $uploadError === UPLOAD_ERR_FORM_SIZE) {
Http::error(
'Die Datei überschreitet das Upload-Limit dieses Servers (aktuell ' . ini_get('upload_max_filesize') .
'). Bitte den Hosting-Anbieter um ein höheres PHP-Upload-Limit bitten.',
422
);
}
if ($uploadError !== UPLOAD_ERR_OK) {
Http::error('Datei-Upload fehlgeschlagen (Fehlercode ' . $uploadError . ').', 422);
}
$raw = file_get_contents($_FILES['file']['tmp_name']);
} else {
// Ein Upload, der post_max_size ueberschreitet, wird von PHP nicht in $_FILES
// aufgenommen - der rohe Body ist ueber php://input aber trotzdem da (nur eben
// noch als unverarbeitetes Multipart-Gemisch, kein gueltiges JSON). Deshalb direkt
// Content-Length gegen das konfigurierte Limit pruefen, statt block auf einen
// (nicht garantiert leeren) Body zu vertrauen - sonst landet eine zu grosse, aber
// eigentlich intakte Export-Datei im irrefuehrenden generischen "ungueltige Datei"-Fehler.
$contentLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
$maxPost = self::iniBytes((string) ini_get('post_max_size'));
if ($maxPost > 0 && $contentLength > $maxPost) {
Http::error(
'Die Datei überschreitet das Größen-Limit dieses Servers (aktuell ' . ini_get('post_max_size') .
'). Bitte den Hosting-Anbieter um ein höheres PHP-Upload-Limit (post_max_size) bitten.',
422
);
}
$raw = file_get_contents('php://input');
}