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

O Operator
Diff

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

added on local at 2026-07-11 18:36:34

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