Diff -- /var/www/vhosts/webstls.com/httpdocs/upload_image.cgi
Diff

/var/www/vhosts/webstls.com/httpdocs/upload_image.cgi

added on WebSTLs (webstls.com) at 2026-07-01 22:27:10

Added
+154
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 94dcb571bb21
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# WebSTLs -- image upload endpoint for the page editor.
4#
5# Accepts multipart/form-data POST with field "image". On success:
6# 1. Looks up the user's storefront.
7# 2. Validates the file (JPG/PNG/GIF/WebP, max 5MB) by magic bytes.
8# 3. Generates a 16-char random id used in the public URL.
9# 4. Saves bytes to httpdocs/uploads/<storefront_id>/<id>.<ext>.
10# 5. INSERTs a page_images row mapping id -> on-disk file.
11# 6. Returns JSON { success: 1, url: "/img.cgi?id=<id>" }.
12#
13# The on-disk path is NEVER exposed in the response. The client only
14# sees /img.cgi?id=<token>; img.cgi resolves the path and streams the
15# bytes. A .htaccess in /uploads/ blocks direct HTTP access as a
16# defense-in-depth measure.
17#======================================================================
18use strict;
19use warnings;
20
21use lib '/var/www/vhosts/webstls.com/httpdocs';
22use CGI;
23# Allow uploads up to 12 MB at the CGI layer. The on-disk size check
24# below still caps at 10 MB; this just keeps CGI from silently truncating
25# the body before we can inspect it.
26$CGI::POST_MAX = 12 * 1024 * 1024;
27use MODS::DBConnect;
28use MODS::Login;
29use MODS::WebSTLs::Config;
30
31$| = 1; # flush stdout so partial output reaches Apache before any exit
32
33my $q = CGI->new;
34my $db = MODS::DBConnect->new;
35my $auth = MODS::Login->new;
36my $config = MODS::WebSTLs::Config->new;
37my $DB = $config->settings('database_name');
38
39# Always emit JSON, even on error -- the editor's fetch() expects it.
40print "Content-Type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
41
42# Safety net: if the script exits mid-flight (e.g., DBConnect's error()
43# helper hits exit on a failing SQL call), an END block fires and emits
44# a JSON error instead of the otherwise-empty body that produces the
45# editor's "server returned 200 (empty body)" message.
46our $UPLOAD_DONE = 0;
47END {
48 if (!$UPLOAD_DONE) {
49 print qq~{"success":0,"error":"server aborted before completing the upload -- check MODS/DB_ERRORS.log for the failing SQL"}~;
50 }
51}
52
53sub fail {
54 my ($msg) = @_;
55 $msg =~ s/"/\\"/g;
56 print qq~{"success":0,"error":"$msg"}~;
57 $UPLOAD_DONE = 1;
58 exit;
59}
60
61# If CGI::POST_MAX was exceeded, $q->cgi_error returns a status string
62# and $q->upload returns nothing. Surface that as JSON so the editor
63# shows a clean message instead of "Unexpected end of JSON input".
64if (my $cgi_err = $q->cgi_error) {
65 fail("upload rejected: $cgi_err");
66}
67
68my $userinfo = $auth->login_verify();
69fail('not authenticated') unless $userinfo && $userinfo->{user_id};
70require MODS::WebSTLs::Permissions;
71fail('not authorized') unless MODS::WebSTLs::Permissions->new->has_feature($userinfo, 'edit_models');
72
73my $uid = $userinfo->{user_id};
74$uid =~ s/[^0-9]//g;
75fail('bad user id') unless $uid;
76
77# Look up the user's storefront. Single-storefront-per-user for now.
78my $dbh = $db->db_connect();
79my $store = $db->db_readwrite($dbh,
80 qq~SELECT id FROM `${DB}`.storefronts WHERE user_id='$uid' ORDER BY id LIMIT 1~,
81 $ENV{SCRIPT_NAME}, __LINE__);
82unless ($store && $store->{id}) {
83 $db->db_disconnect($dbh);
84 fail('no storefront found for this user');
85}
86my $sid = $store->{id};
87
88# ---- Receive + validate the file ----
89my $uploaded = $q->upload('image');
90unless ($uploaded) { $db->db_disconnect($dbh); fail('no file received'); }
91
92my $tmpname = $q->tmpFileName($uploaded);
93unless ($tmpname && -f $tmpname) { $db->db_disconnect($dbh); fail('temp file missing'); }
94
95my $size = -s $tmpname;
96unless ($size && $size > 0) { $db->db_disconnect($dbh); fail('empty file'); }
97if ($size > 10 * 1024 * 1024) { $db->db_disconnect($dbh); fail('file too large (max 10MB)'); }
98
99open(my $fh, '<', $tmpname) or do { $db->db_disconnect($dbh); fail('cannot read temp'); };
100binmode $fh;
101read($fh, my $head, 16);
102close $fh;
103
104my ($ext, $mime);
105if ($head =~ /^\xFF\xD8\xFF/) { $ext = 'jpg'; $mime = 'image/jpeg'; }
106elsif ($head =~ /^\x89PNG\r\n\x1A\n/) { $ext = 'png'; $mime = 'image/png'; }
107elsif ($head =~ /^GIF8[79]a/) { $ext = 'gif'; $mime = 'image/gif'; }
108elsif ($head =~ /^RIFF.{4}WEBP/s) { $ext = 'webp'; $mime = 'image/webp'; }
109else { $db->db_disconnect($dbh); fail('not a recognized image (JPG/PNG/GIF/WebP)'); }
110
111# ---- Generate a random 16-char id used in /img.cgi?id=<id> ----
112my @hex = ('0'..'9', 'a'..'f');
113my $id = '';
114$id .= $hex[int(rand(@hex))] for 1..16;
115
116# ---- Save the file under uploads/<storefront_id>/<id>.<ext> ----
117my $base_dir = '/var/www/vhosts/webstls.com/httpdocs/uploads';
118my $store_dir = "$base_dir/$sid";
119my $dest_path = "$store_dir/$id.$ext";
120my $filename = "$id.$ext";
121
122unless (-d $base_dir) {
123 mkdir $base_dir, 0755 or do { $db->db_disconnect($dbh); fail("mkdir base: $!"); };
124}
125unless (-d $store_dir) {
126 mkdir $store_dir, 0755 or do { $db->db_disconnect($dbh); fail("mkdir store: $!"); };
127}
128
129open(my $in_fh, '<', $tmpname) or do { $db->db_disconnect($dbh); fail("open temp: $!"); };
130open(my $out_fh, '>', $dest_path) or do { $db->db_disconnect($dbh); fail("open dest: $!"); };
131binmode $in_fh;
132binmode $out_fh;
133while (read($in_fh, my $buf, 8192)) {
134 print $out_fh $buf;
135}
136close $in_fh;
137close $out_fh;
138
139# ---- DB insert mapping id -> file ----
140$db->db_readwrite($dbh, qq~
141 INSERT INTO `${DB}`.page_images SET
142 id = '$id',
143 user_id = '$uid',
144 storefront_id = '$sid',
145 filename = '$filename',
146 mime_type = '$mime',
147 size_bytes = '$size'
148~, $ENV{SCRIPT_NAME}, __LINE__);
149
150$db->db_disconnect($dbh);
151
152print qq~{"success":1,"url":"/img.cgi?id=$id","size":$size}~;
153$UPLOAD_DONE = 1;
154exit;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help