Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/support.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/support.cgi

added on local at 2026-07-11 18:37:03

Added
+661
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7f5ec0970d30
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# ABForge -- customer support center + related endpoints.
4#
5# Dispatches on SCRIPT_NAME:
6# /support.cgi -- two-pane HTML support console
7# /support_file.cgi -- stream attachment bytes (image serving)
8# /support_poll.cgi -- JSON poll endpoint for chat refresh
9# /support_send.cgi -- POST handler to send a reply (JSON or 302)
10# /support_upload.cgi -- JSON upload endpoint for attachments
11#
12# Each *_action wrapper is a 3-line `do` shim that re-enters this file.
13# The dispatch fans out early -- everything below the dispatch that
14# needs support.cgi-specific state (e.g. wrapper render) is guarded.
15#======================================================================
16
17# ---- Upload endpoint has to install its END-guard + JSON header
18# before we even load CGI (POST_MAX bumps the ceiling). Do that first
19# when the entry is support_upload.
20BEGIN {
21 my $sn = $ENV{SCRIPT_NAME} || '';
22 if ($sn =~ m{/support_upload\.cgi$}) {
23 $ENV{__SUP_ENTRY} = 'support_upload';
24 }
25}
26
27use strict;
28use warnings;
29
30use lib '/var/www/vhosts/3dshawn.com/abforge.3dshawn.com';
31use CGI;
32if (($ENV{__SUP_ENTRY} || '') eq 'support_upload') {
33 $CGI::POST_MAX = 12 * 1024 * 1024;
34}
35use MODS::Template;
36use MODS::DBConnect;
37use MODS::Login;
38use MODS::ABForge::Config;
39use MODS::ABForge::Wrapper;
40use MODS::ABForge::Support;
41
42our $UPLOAD_DONE = 0;
43END {
44 if (!$UPLOAD_DONE && ($ENV{SCRIPT_NAME}||'') =~ m{/support_upload\.cgi$}) {
45 print qq~{"success":0,"error":"server aborted before completing the upload"}~;
46 }
47}
48
49my $q = CGI->new;
50my $form = $q->Vars;
51my $tfile = MODS::Template->new;
52my $db = MODS::DBConnect->new;
53my $auth = MODS::Login->new;
54my $config = MODS::ABForge::Config->new;
55my $wrap = MODS::ABForge::Wrapper->new;
56my $sup = MODS::ABForge::Support->new;
57my $DB = $config->settings('database_name');
58
59$| = 1;
60
61my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'support';
62
63# ---- Route to sub-endpoints -----------------------------------------
64if ($entry eq 'support_file') { _sup_file(); exit; }
65if ($entry eq 'support_poll') { _sup_poll(); exit; }
66if ($entry eq 'support_send') { _sup_send(); exit; }
67if ($entry eq 'support_upload') { _sup_upload(); exit; }
68
69# ================ MAIN support.cgi page ==============================
70my $userinfo = $auth->login_verify();
71unless ($userinfo) {
72 print "Status: 302 Found\nLocation: /login.cgi?return=/support.cgi\n\n";
73 exit;
74}
75
76my $uid = $userinfo->{user_id};
77$uid =~ s/[^0-9]//g;
78
79my $dbh = $db->db_connect();
80
81# ---- POST handlers (PRG pattern) -----------------------------------
82if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
83 my $action = $form->{action} || '';
84 if ($action eq 'create_thread') {
85 my $subject = $form->{subject} || '';
86 my $body = $form->{body} || '';
87 $subject =~ s/^\s+|\s+$//g; $body =~ s/^\s+|\s+$//g;
88 if (length $subject && length $body) {
89 my $tid = $sup->create_thread($db, $dbh, $DB, {
90 user_id => $uid, subject => $subject, body => $body,
91 });
92 if ($tid) {
93 my @ids = grep { /^[a-zA-Z0-9]{16}$/ } $q->multi_param('attach_id');
94 if (@ids) {
95 my $first_msg = $db->db_readwrite($dbh,
96 qq~SELECT id FROM `${DB}`.support_messages
97 WHERE thread_id='$tid' ORDER BY id ASC LIMIT 1~,
98 $ENV{SCRIPT_NAME}, __LINE__);
99 my $mid = ($first_msg && $first_msg->{id}) ? $first_msg->{id} : 0;
100 if ($mid) {
101 my $in_list = join(',', map { "'$_'" } @ids);
102 $db->db_readwrite($dbh, qq~
103 UPDATE `${DB}`.support_attachments
104 SET thread_id='$tid', message_id='$mid'
105 WHERE id IN ($in_list)
106 AND user_id='$uid'
107 AND thread_id IS NULL
108 ~, $ENV{SCRIPT_NAME}, __LINE__);
109 }
110 }
111 $db->db_disconnect($dbh);
112 print "Status: 302 Found\nLocation: /support.cgi?thread_id=$tid&ok=opened\n\n";
113 exit;
114 }
115 }
116 $db->db_disconnect($dbh);
117 print "Status: 302 Found\nLocation: /support.cgi?new=1&err=missing\n\n";
118 exit;
119 }
120}
121
122# ---- GET render ----------------------------------------------------
123my $threads = $sup->list_threads_for_customer($db, $dbh, $DB, $uid);
124
125# Active thread (if any)
126my $active_tid = $form->{thread_id} || 0;
127$active_tid =~ s/[^0-9]//g;
128my $active_thread;
129my $messages = [];
130if ($active_tid) {
131 $active_thread = $sup->load_thread($db, $dbh, $DB, $active_tid);
132 if (!$active_thread || $active_thread->{customer_user_id} ne $uid) {
133 $active_thread = undef;
134 $active_tid = 0;
135 } else {
136 $messages = $sup->load_messages($db, $dbh, $DB, $active_tid);
137 $sup->mark_read($db, $dbh, $DB, $active_tid, 'customer') if $active_thread->{customer_unread};
138 }
139}
140
141$db->db_disconnect($dbh);
142
143my @thread_tiles;
144foreach my $t (@$threads) {
145 my $is_active = ($active_tid && $t->{id} eq $active_tid) ? 1 : 0;
146 push @thread_tiles, {
147 id => $t->{id},
148 subject => _sup_h($t->{subject}),
149 status => $t->{status},
150 status_label => _sup_status_label($t->{status}),
151 status_class => _sup_status_class($t->{status}),
152 unread => $t->{customer_unread} || 0,
153 has_unread => ($t->{customer_unread} && !$is_active) ? 1 : 0,
154 is_active => $is_active,
155 last_at => _sup_humanize_ts($t->{last_message_at}),
156 };
157}
158
159my @message_rows;
160if ($active_thread) {
161 my $atts_by_msg = {};
162 my $dbh_a = $db->db_connect();
163 if ($dbh_a) {
164 my @rows = $db->db_readwrite_multiple($dbh_a, qq~
165 SELECT id, message_id, filename, mime_type, size_bytes
166 FROM `${DB}`.support_attachments
167 WHERE thread_id='$active_tid'
168 ORDER BY uploaded_at ASC
169 ~, $ENV{SCRIPT_NAME}, __LINE__);
170 $db->db_disconnect($dbh_a);
171 for my $a (@rows) {
172 push @{ $atts_by_msg->{ $a->{message_id} } }, {
173 id => $a->{id},
174 url => "/support_file.cgi?id=$a->{id}",
175 name => $a->{filename},
176 };
177 }
178 }
179 foreach my $m (@$messages) {
180 my $role = $m->{sender_role};
181 my $atts = $atts_by_msg->{ $m->{id} } || [];
182 push @message_rows, {
183 id => $m->{id},
184 body_html => _sup_body_html($m->{body}),
185 ts => _sup_humanize_ts($m->{created_at}),
186 ts_iso => $m->{created_at},
187 sender_name => _sup_h($m->{sender_name} || ($role eq 'system' ? 'ABForge' : 'Anonymous')),
188 sender_initial => uc(substr($m->{sender_name} || ($role eq 'admin' ? 'W' : 'C'), 0, 1)),
189 is_customer => ($role eq 'customer') ? 1 : 0,
190 is_admin => ($role eq 'admin') ? 1 : 0,
191 is_system => ($role eq 'system') ? 1 : 0,
192 attachments => $atts,
193 has_attach => scalar(@$atts) ? 1 : 0,
194 };
195 }
196}
197
198my $tvars = {
199 threads => \@thread_tiles,
200 has_threads => scalar(@thread_tiles) ? 1 : 0,
201 thread_count => scalar(@thread_tiles),
202
203 has_active => $active_thread ? 1 : 0,
204 active_id => $active_thread ? $active_thread->{id} : 0,
205 active_subject => $active_thread ? _sup_h($active_thread->{subject}) : '',
206 active_status => $active_thread ? _sup_status_label($active_thread->{status}) : '',
207 active_status_class => $active_thread ? _sup_status_class($active_thread->{status}) : '',
208 messages => \@message_rows,
209 has_messages => scalar(@message_rows) ? 1 : 0,
210 last_message_id => scalar(@message_rows) ? $message_rows[-1]->{id} : 0,
211
212 show_new_form => $form->{new} ? 1 : 0,
213 new_err => $form->{err} || '',
214 has_new_err => ($form->{err}) ? 1 : 0,
215 just_opened => (($form->{ok} || '') eq 'opened') ? 1 : 0,
216};
217
218my $body = join('', $tfile->template('abforge_support.html', $tvars, $userinfo));
219
220print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
221
222$wrap->render({
223 userinfo => $userinfo,
224 page_key => 'support',
225 title => 'Support',
226 body => $body,
227});
228
229exit;
230
231#======================================================================
232# Shared helpers (used by main page + JSON endpoints)
233#======================================================================
234sub _sup_h {
235 my $s = shift; $s = '' unless defined $s;
236 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
237 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
238 return $s;
239}
240
241sub _sup_body_html {
242 my $s = shift; $s = '' unless defined $s;
243 $s = _sup_h($s);
244 $s =~ s/\$/\\\$/g;
245 $s =~ s/\@/\\\@/g;
246 $s =~ s/\r//g;
247 $s =~ s/\n/<br>/g;
248 return $s;
249}
250
251sub _sup_status_label {
252 my $s = shift || '';
253 return 'Open' if $s eq 'open';
254 return 'Awaiting reply' if $s eq 'waiting_admin';
255 return 'Waiting on you' if $s eq 'waiting_customer';
256 return 'Resolved' if $s eq 'resolved';
257 return 'Closed' if $s eq 'closed';
258 return $s;
259}
260sub _sup_status_class {
261 my $s = shift || '';
262 return 'st-open' if $s eq 'open' || $s eq 'waiting_admin';
263 return 'st-waiting' if $s eq 'waiting_customer';
264 return 'st-resolved' if $s eq 'resolved';
265 return 'st-closed' if $s eq 'closed';
266 return 'st-open';
267}
268
269sub _sup_humanize_ts {
270 my $t = shift; $t = '' unless defined $t;
271 return '' unless $t =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
272 my ($y, $mo, $d, $hh, $mm, $ss) = ($1, $2, $3, $4, $5, $6);
273 require Time::Local;
274 my $then = eval { Time::Local::timelocal($ss, $mm, $hh, $d, $mo - 1, $y - 1900) } || 0;
275 my $diff = time - $then;
276 my $human;
277 if ($diff < 60) { $human = 'Just now'; }
278 elsif ($diff < 3600) { $human = int($diff/60) . 'm ago'; }
279 elsif ($diff < 86400) { $human = int($diff/3600) . 'h ago'; }
280 elsif ($diff < 172800) { $human = 'Yesterday'; }
281 elsif ($diff < 604800) { $human = int($diff/86400) . 'd ago'; }
282 else {
283 my @mn = ('','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
284 $human = "$mn[int($mo)] $d";
285 }
286 # Viewer-local rewrite via portfolio window.tzLocalize(). data-fmt
287 # 'relative' lets the client recompute the "5m ago" phrasing from
288 # the viewer's wall clock instead of the server's TZ.
289 return $then
290 ? qq~<span class="ts" data-ts="$then" data-fmt="relative">$human</span>~
291 : $human;
292}
293
294#======================================================================
295# support_file.cgi ?id=<16-char> -- stream attachment bytes.
296#======================================================================
297sub _sup_file {
298 my $id = $q->param('id') || '';
299 $id =~ s/[^a-zA-Z0-9]//g;
300 _sup_file_notfound() unless length($id) == 16;
301
302 my $userinfo_f = $auth->login_verify();
303 _sup_file_forbidden() unless $userinfo_f && $userinfo_f->{user_id};
304 my $viewer_uid = $userinfo_f->{user_id};
305 $viewer_uid =~ s/[^0-9]//g;
306 my $is_admin = $userinfo_f->{is_admin} ? 1 : 0;
307
308 my $dbh_f = $db->db_connect();
309 my $row = $db->db_readwrite($dbh_f, qq~
310 SELECT a.user_id AS uploader_id, a.thread_id, a.filename, a.mime_type, a.size_bytes,
311 t.customer_user_id
312 FROM `${DB}`.support_attachments a
313 LEFT JOIN `${DB}`.support_threads t ON t.id = a.thread_id
314 WHERE a.id='$id' LIMIT 1
315 ~, $ENV{SCRIPT_NAME}, __LINE__);
316 $db->db_disconnect($dbh_f);
317
318 _sup_file_notfound() unless $row && $row->{filename};
319
320 my $allowed = 0;
321 $allowed = 1 if $is_admin;
322 $allowed = 1 if $row->{uploader_id} eq $viewer_uid;
323 $allowed = 1 if $row->{customer_user_id} && $row->{customer_user_id} eq $viewer_uid;
324 _sup_file_forbidden() unless $allowed;
325
326 my $filename = $row->{filename};
327 _sup_file_notfound() if $filename =~ m{[/\\]} || $filename =~ /\.\./;
328
329 my $path = "/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/uploads/support/$filename";
330 _sup_file_notfound() unless -r $path;
331
332 my %ok_mime = map { $_ => 1 } qw(image/jpeg image/png image/gif image/webp image/bmp);
333 my $mime = $row->{mime_type} || 'application/octet-stream';
334 $mime = 'application/octet-stream' unless $ok_mime{$mime};
335
336 my $size = -s $path;
337 print "Content-Type: $mime\n";
338 print "Content-Length: $size\n";
339 print "Cache-Control: private, max-age=31536000, immutable\n";
340 print "X-Content-Type-Options: nosniff\n";
341 print "\n";
342
343 binmode STDOUT;
344 open(my $fh, '<', $path) or _sup_file_notfound();
345 binmode $fh;
346 my $buf;
347 while (read($fh, $buf, 65536)) { print STDOUT $buf; }
348 close $fh;
349 return;
350}
351
352sub _sup_file_notfound {
353 print "Status: 404 Not Found\nContent-Type: text/plain\n\nnot found\n";
354 exit;
355}
356sub _sup_file_forbidden {
357 print "Status: 403 Forbidden\nContent-Type: text/plain\n\nforbidden\n";
358 exit;
359}
360
361#======================================================================
362# support_poll.cgi -- JSON poll endpoint for message refresh.
363#======================================================================
364sub _sup_poll {
365 my $userinfo_p = $auth->login_verify();
366 _sup_json_fail(401, 'not authenticated') unless $userinfo_p && $userinfo_p->{user_id};
367
368 my $uid_p = $userinfo_p->{user_id};
369 $uid_p =~ s/[^0-9]//g;
370 my $is_admin = $userinfo_p->{is_admin} ? 1 : 0;
371
372 my $tid = $form->{thread_id} || 0; $tid =~ s/[^0-9]//g;
373 _sup_json_fail(400, 'missing thread_id') unless $tid;
374 my $since = $form->{since} || 0; $since =~ s/[^0-9]//g;
375
376 my $dbh_p = $db->db_connect();
377 my $thread = $sup->load_thread($db, $dbh_p, $DB, $tid);
378 unless ($thread && $thread->{id}) {
379 $db->db_disconnect($dbh_p);
380 _sup_json_fail(404, 'thread not found');
381 }
382 unless ($is_admin || $thread->{customer_user_id} eq $uid_p) {
383 $db->db_disconnect($dbh_p);
384 _sup_json_fail(403, 'not your thread');
385 }
386
387 my $rows = $sup->load_messages($db, $dbh_p, $DB, $tid, $since);
388
389 my %atts_by_msg;
390 if (@$rows) {
391 my @msg_ids = map { $_->{id} } @$rows;
392 my $have_at = $db->db_readwrite($dbh_p, qq~
393 SELECT COUNT(*) AS n FROM information_schema.tables
394 WHERE table_schema='$DB' AND table_name='support_attachments'
395 ~, $ENV{SCRIPT_NAME}, __LINE__);
396 if ($have_at && $have_at->{n}) {
397 my $in_list = join(',', map { "'$_'" } @msg_ids);
398 my @arows = $db->db_readwrite_multiple($dbh_p, qq~
399 SELECT id, message_id, filename
400 FROM `${DB}`.support_attachments
401 WHERE message_id IN ($in_list)
402 ORDER BY uploaded_at ASC
403 ~, $ENV{SCRIPT_NAME}, __LINE__);
404 for my $a (@arows) {
405 push @{ $atts_by_msg{ $a->{message_id} } }, {
406 id => $a->{id},
407 url => "/support_file.cgi?id=$a->{id}",
408 name => $a->{filename},
409 };
410 }
411 }
412 }
413
414 my $side = $is_admin ? 'admin' : 'customer';
415 $sup->mark_read($db, $dbh_p, $DB, $tid, $side);
416
417 my $fresh = $sup->load_thread($db, $dbh_p, $DB, $tid);
418 $db->db_disconnect($dbh_p);
419
420 my $last_id = 0;
421 my @msg_json;
422 foreach my $m (@$rows) {
423 $last_id = $m->{id} if $m->{id} > $last_id;
424 my $body = $m->{body};
425 $body =~ s/&/&amp;/g; $body =~ s/</&lt;/g; $body =~ s/>/&gt;/g;
426 $body =~ s/"/&quot;/g; $body =~ s/'/&#39;/g;
427 $body =~ s/\r//g; $body =~ s/\n/<br>/g;
428
429 my $name = $m->{sender_name} || ($m->{sender_role} eq 'system' ? 'ABForge' : 'User');
430 my $atts = $atts_by_msg{ $m->{id} } || [];
431 if (@$atts) {
432 $body .= '<div class="sup-msg-atts">';
433 for my $a (@$atts) {
434 my $aid = $a->{id};
435 $body .= qq~<a href="/support_file.cgi?id=$aid" target="_blank" rel="noopener" class="sup-msg-att"><img src="/support_file.cgi?id=$aid" alt=""></a>~;
436 }
437 $body .= '</div>';
438 }
439 push @msg_json, _sup_json_obj({
440 id => $m->{id} + 0,
441 sender_role => $m->{sender_role},
442 sender_name => $name,
443 body_html => $body,
444 ts => $m->{created_at} || '',
445 });
446 }
447 $last_id = $since if $last_id < $since;
448
449 my $thread_status = $fresh ? ($fresh->{status} || '') : '';
450
451 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
452 print "{\"ok\":true,";
453 print "\"thread_id\":$tid,";
454 print "\"last_id\":$last_id,";
455 print "\"status\":\"" . _sup_json_str($thread_status) . "\",";
456 print "\"messages\":[" . join(',', @msg_json) . "]";
457 print "}";
458 return;
459}
460
461sub _sup_json_fail {
462 my ($code, $msg) = @_;
463 print "Status: $code\nContent-Type: application/json\nCache-Control: no-cache\n\n";
464 $msg =~ s/"/\\"/g;
465 print qq~{"ok":false,"error":"$msg"}~;
466 exit;
467}
468
469sub _sup_json_str {
470 my $s = shift; $s = '' unless defined $s;
471 $s =~ s/\\/\\\\/g;
472 $s =~ s/"/\\"/g;
473 $s =~ s/\r//g;
474 $s =~ s/\n/\\n/g;
475 $s =~ s/\t/\\t/g;
476 $s =~ s/[\x00-\x1f]//g;
477 return $s;
478}
479sub _sup_json_obj {
480 my $h = shift;
481 my @kv;
482 foreach my $k (sort keys %$h) {
483 my $v = $h->{$k};
484 if (defined $v && $v =~ /^-?\d+$/) {
485 push @kv, qq~"$k":$v~;
486 } else {
487 push @kv, qq~"$k":"~ . _sup_json_str($v) . qq~"~;
488 }
489 }
490 return '{' . join(',', @kv) . '}';
491}
492
493#======================================================================
494# support_send.cgi -- POST handler to send a message reply.
495#======================================================================
496sub _sup_send {
497 my $async = $form->{async} ? 1 : 0;
498
499 my $userinfo_s = $auth->login_verify();
500 _sup_send_fail($async, 401, 'not authenticated') unless $userinfo_s && $userinfo_s->{user_id};
501
502 my $uid_s = $userinfo_s->{user_id};
503 $uid_s =~ s/[^0-9]//g;
504 my $is_admin = $userinfo_s->{is_admin} ? 1 : 0;
505
506 my $tid = $form->{thread_id} || 0;
507 $tid =~ s/[^0-9]//g;
508 _sup_send_fail($async, 400, 'missing thread_id') unless $tid;
509
510 my $body = $form->{body} || '';
511 $body =~ s/^\s+|\s+$//g;
512 _sup_send_fail($async, 400, 'empty message') unless length $body;
513 $body = substr($body, 0, 50000);
514
515 my $dbh_s = $db->db_connect();
516
517 my $thread = $sup->load_thread($db, $dbh_s, $DB, $tid);
518 unless ($thread && $thread->{id}) {
519 $db->db_disconnect($dbh_s);
520 _sup_send_fail($async, 404, 'thread not found');
521 }
522
523 my $sender_role;
524 if ($is_admin) {
525 $sender_role = 'admin';
526 } else {
527 if ($thread->{customer_user_id} ne $uid_s) {
528 $db->db_disconnect($dbh_s);
529 _sup_send_fail($async, 403, 'not your thread');
530 }
531 $sender_role = 'customer';
532 }
533
534 my $new_msg_id = $sup->send_message($db, $dbh_s, $DB, {
535 thread_id => $tid,
536 sender_role => $sender_role,
537 sender_user_id => $uid_s,
538 body => $body,
539 });
540
541 my @ids = grep { /^[a-zA-Z0-9]{16}$/ } $q->multi_param('attach_id');
542 if (@ids && $new_msg_id) {
543 my $in_list = join(',', map { "'$_'" } @ids);
544 $db->db_readwrite($dbh_s, qq~
545 UPDATE `${DB}`.support_attachments
546 SET thread_id='$tid', message_id='$new_msg_id'
547 WHERE id IN ($in_list)
548 AND user_id='$uid_s'
549 AND thread_id IS NULL
550 ~, $ENV{SCRIPT_NAME}, __LINE__);
551 }
552
553 $sup->mark_read($db, $dbh_s, $DB, $tid, $sender_role);
554
555 $db->db_disconnect($dbh_s);
556
557 if ($async) {
558 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
559 print qq~{"ok":true,"message_id":$new_msg_id,"thread_id":$tid}~;
560 } else {
561 my $back = $is_admin ? "/admin_messages.cgi" : "/support.cgi";
562 print "Status: 302 Found\nLocation: $back?thread_id=$tid\n\n";
563 }
564 return;
565}
566
567sub _sup_send_fail {
568 my ($async, $code, $msg) = @_;
569 if ($async) {
570 print "Status: $code\nContent-Type: application/json\n\n";
571 $msg =~ s/"/\\"/g;
572 print qq~{"ok":false,"error":"$msg"}~;
573 } else {
574 print "Status: $code\nContent-Type: text/plain\n\n$msg\n";
575 }
576 exit;
577}
578
579#======================================================================
580# support_upload.cgi -- JSON attachment upload endpoint.
581#======================================================================
582sub _sup_upload {
583 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
584
585 if (my $cgi_err = $q->cgi_error) {
586 _sup_upload_fail("upload rejected: $cgi_err");
587 }
588
589 my $userinfo_u = $auth->login_verify();
590 _sup_upload_fail('not authenticated') unless $userinfo_u && $userinfo_u->{user_id};
591 my $uid_u = $userinfo_u->{user_id};
592 $uid_u =~ s/[^0-9]//g;
593 _sup_upload_fail('bad user id') unless $uid_u;
594
595 my $uploaded = $q->upload('image');
596 _sup_upload_fail('no file received') unless $uploaded;
597
598 my $tmpname = $q->tmpFileName($uploaded);
599 _sup_upload_fail('temp file missing') unless $tmpname && -f $tmpname;
600
601 my $size = -s $tmpname;
602 _sup_upload_fail('empty file') unless $size && $size > 0;
603 _sup_upload_fail('file too large (max 10MB)') if $size > 10 * 1024 * 1024;
604
605 open(my $fh, '<', $tmpname) or _sup_upload_fail('cannot read temp');
606 binmode $fh;
607 read($fh, my $head, 16);
608 close $fh;
609
610 my ($ext, $mime);
611 if ($head =~ /^\xFF\xD8\xFF/) { $ext = 'jpg'; $mime = 'image/jpeg'; }
612 elsif ($head =~ /^\x89PNG\r\n\x1A\n/) { $ext = 'png'; $mime = 'image/png'; }
613 elsif ($head =~ /^GIF8[79]a/) { $ext = 'gif'; $mime = 'image/gif'; }
614 elsif ($head =~ /^RIFF.{4}WEBP/s) { $ext = 'webp'; $mime = 'image/webp'; }
615 elsif ($head =~ /^BM/) { $ext = 'bmp'; $mime = 'image/bmp'; }
616 else { _sup_upload_fail('not a recognized image (JPG/PNG/GIF/WebP/BMP)'); }
617
618 my @hex = ('0'..'9', 'a'..'f');
619 my $id = '';
620 $id .= $hex[int(rand(@hex))] for 1..16;
621 my $filename = "$id.$ext";
622
623 my $base_dir = '/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/uploads';
624 my $sup_dir = "$base_dir/support";
625 my $dest_path = "$sup_dir/$filename";
626
627 unless (-d $base_dir) { mkdir $base_dir, 0755 or _sup_upload_fail("mkdir base: $!"); }
628 unless (-d $sup_dir) { mkdir $sup_dir, 0755 or _sup_upload_fail("mkdir support: $!"); }
629
630 open(my $in_fh, '<', $tmpname) or _sup_upload_fail("open temp: $!");
631 open(my $out_fh, '>', $dest_path) or _sup_upload_fail("open dest: $!");
632 binmode $in_fh;
633 binmode $out_fh;
634 while (read($in_fh, my $buf, 8192)) { print $out_fh $buf; }
635 close $in_fh;
636 close $out_fh;
637
638 my $dbh_u = $db->db_connect();
639 $db->db_readwrite($dbh_u, qq~
640 INSERT INTO `${DB}`.support_attachments SET
641 id = '$id',
642 user_id = '$uid_u',
643 filename = '$filename',
644 mime_type = '$mime',
645 size_bytes = '$size'
646 ~, $ENV{SCRIPT_NAME}, __LINE__);
647 $db->db_disconnect($dbh_u);
648
649 my $size_kb = int($size / 1024);
650 print qq~{"success":1,"id":"$id","url":"/support_file.cgi?id=$id","filename":"$filename","size_kb":$size_kb,"mime":"$mime"}~;
651 $UPLOAD_DONE = 1;
652 return;
653}
654
655sub _sup_upload_fail {
656 my ($msg) = @_;
657 $msg =~ s/"/\\"/g;
658 print qq~{"success":0,"error":"$msg"}~;
659 $UPLOAD_DONE = 1;
660 exit;
661}