From 7767435f829f6e945372d1c39139271bfc64ce69 Mon Sep 17 00:00:00 2001 From: ilotterytea Date: Sun, 16 Nov 2025 17:59:16 +0500 Subject: feat: ZIP web applications --- lib/file.php | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- public/index.php | 15 +++++---- public/upload.php | 12 +++++++ 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/lib/file.php b/lib/file.php index ebfc009..65a2abe 100644 --- a/lib/file.php +++ b/lib/file.php @@ -150,4 +150,95 @@ function remove_video_letterbox(string $input_path, string $output_path) exec("ffmpeg -nostdin -i $input_path -vf $area -c:a copy $output_path 2>&1", $output, $code); return $code == 0; -} \ No newline at end of file +} + +function parse_zip_web_archive(string $input_path, string $output_path) +{ + $allowed_extensions = [ + "html", + "js", + "css", + "png", + "jpg", + "jpeg", + "gif", + "mp3", + "ogg", + "wasm", + "atlas", + "skin", + "txt", + "fnt", + "json", + "glb", + "glsl", + "map", + "teavmdbg", + "xml", + "ds_store", + ]; + $max_total_uncompressed = 128 * 1024 * 1024; + $max_file_size = 32 * 1024 * 1024; + + $zip = new ZipArchive(); + if ($zip->open($input_path) !== true) { + throw new RuntimeException("Invalid ZIP"); + } + + $is_webapp = false; + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + $is_webapp = $stat["name"] == "index.html"; + if ($is_webapp) { + break; + } + } + + if (!$is_webapp) { + $zip->close(); + return $is_webapp; + } + + $total_uncompressed = 0; + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + $path = $stat["name"]; + if (strpos($path, "..") !== false) { + throw new RuntimeException("Invalid file path"); + } + + if ( + strpos($path, "__MACOSX/") === 0 || + (basename($path)[0] === "." && + strstr(basename($path), 0, 2) === "._") + ) { + continue; + } + + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + error_log($ext); + if (!in_array($ext, $allowed_extensions) && $stat["size"] > 0) { + throw new RuntimeException( + "Forbidden file type in the archive: $path", + ); + } + + $total_uncompressed += $stat["size"]; + if ( + $total_uncompressed > $max_total_uncompressed || + $stat["size"] > $max_file_size + ) { + throw new RuntimeException("ZIP too large when uncompressed"); + } + } + + mkdir($output_path, 0755, true); + if (!$zip->extractTo($output_path)) { + rmdir($output_path); + throw new RuntimeException("ZIP extraction failed"); + } + + $zip->close(); + + return $is_webapp; +} diff --git a/public/index.php b/public/index.php index e4542d8..9226066 100644 --- a/public/index.php +++ b/public/index.php @@ -83,9 +83,8 @@ if (FILE_CATALOG_FANCY_VIEW && $file_id) { '); $stmt->execute([$file_id, $file_ext]); $file = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; - $file_exists = is_file(FILE_UPLOAD_DIRECTORY . "/$file_id.$file_ext"); - if (!$file || !$file_exists) { + if (!$file) { http_response_code(404); exit(); } @@ -100,7 +99,6 @@ if (FILE_CATALOG_FANCY_VIEW && $file_id) { $_SESSION['viewed_file_ids'] = $viewed_file_ids; if ( - $file_exists && isset($file['expires_at']) && ( ($file['expires_at'] == $file['uploaded_at'] && $file['views'] > 1) || @@ -223,9 +221,7 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt');

Reason:

- - - +
>
@@ -281,6 +277,13 @@ $privacy_exists = is_file($_SERVER['DOCUMENT_ROOT'] . '/static/PRIVACY.txt'); + + + +
diff --git a/public/upload.php b/public/upload.php index bfa1990..6eda3c6 100644 --- a/public/upload.php +++ b/public/upload.php @@ -214,6 +214,18 @@ try { unlink($input_path); } + if ( + ZIPWEBAPP_ENABLE && + $file_data["extension"] === "zip" && + parse_zip_web_archive( + $file_path, + sprintf("%s/%s", FILE_UPLOAD_DIRECTORY, $file_id), + ) + ) { + $file_data["extension"] = "html"; + $file_data["mime"] = "text/html"; + } + $file_data['size'] = filesize($file_path); if (FILE_THUMBNAILS && !is_dir(FILE_THUMBNAIL_DIRECTORY) && !mkdir(FILE_THUMBNAIL_DIRECTORY, 0777, true)) { -- cgit v1.2.3