Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/support_meta_send.cgi
Diff
/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/support_meta_send.cgi
added on local at 2026-07-10 13:48:40
Added
+110
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7e1015784044
to 7e1015784044
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 | # Cross-site support reply endpoint -- PTMatrix variant. | |
| 4 | # | |
| 5 | # PTMatrix's support_threads/support_messages schema differs from the | |
| 6 | # other 6 sister sites (sender_kind vs sender_role, user_unread vs | |
| 7 | # customer_unread, opened_by_user_id, status enum open|answered|closed) | |
| 8 | # and it has no MODS::PTMatrix::Support module -- reply logic lives in | |
| 9 | # support.cgi's handle_reply sub. Rather than shell out into that sub, | |
| 10 | # this endpoint mirrors handle_reply's DB mutations directly. | |
| 11 | # | |
| 12 | # Wire-format matches the other sites (HMAC + JSON POST): see comments | |
| 13 | # in any sister-site support_meta_send.cgi. | |
| 14 | #====================================================================== | |
| 15 | use strict; | |
| 16 | use warnings; | |
| 17 | ||
| 18 | my $raw = ''; | |
| 19 | if (($ENV{REQUEST_METHOD} || '') eq 'POST') { | |
| 20 | my $len = $ENV{CONTENT_LENGTH} || 0; | |
| 21 | read STDIN, $raw, $len if $len > 0 && $len < 65536; | |
| 22 | } | |
| 23 | ||
| 24 | use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com'; | |
| 25 | use JSON::PP (); | |
| 26 | use Digest::SHA qw(hmac_sha256_hex sha256_hex); | |
| 27 | use MODS::DBConnect; | |
| 28 | use MODS::PTMatrix::Config; | |
| 29 | ||
| 30 | sub _json_out { | |
| 31 | my ($code, $body) = @_; | |
| 32 | my $status_line = $code == 200 ? '' : "Status: $code\n"; | |
| 33 | print "${status_line}Content-Type: application/json\n\n"; | |
| 34 | print JSON::PP::encode_json($body); | |
| 35 | exit; | |
| 36 | } | |
| 37 | ||
| 38 | _json_out(405, { error => 'method_not_allowed' }) | |
| 39 | unless ($ENV{REQUEST_METHOD} || '') eq 'POST'; | |
| 40 | _json_out(400, { error => 'empty_body' }) unless length $raw; | |
| 41 | ||
| 42 | my $sig_hdr = $ENV{HTTP_X_META_SIG} || ''; | |
| 43 | $sig_hdr =~ s/[^a-f0-9]//gi; | |
| 44 | _json_out(400, { error => 'no_signature' }) unless length($sig_hdr) == 64; | |
| 45 | ||
| 46 | my $data = eval { JSON::PP::decode_json($raw) }; | |
| 47 | _json_out(400, { error => 'bad_json' }) unless $data && ref($data) eq 'HASH'; | |
| 48 | ||
| 49 | my $ts = int($data->{ts} || 0); | |
| 50 | my $thread_id = int($data->{thread_id} || 0); | |
| 51 | my $body_txt = defined($data->{body}) ? $data->{body} : ''; | |
| 52 | ||
| 53 | _json_out(400, { error => 'missing_thread_id' }) unless $thread_id > 0; | |
| 54 | _json_out(400, { error => 'empty_body' }) unless length $body_txt; | |
| 55 | _json_out(400, { error => 'stale_timestamp' }) if abs(time - $ts) > 300; | |
| 56 | ||
| 57 | my $cfg = MODS::PTMatrix::Config->new; | |
| 58 | my $secret = $cfg->settings('meta_admin_hmac_secret') || ''; | |
| 59 | _json_out(500, { error => 'no_secret_configured' }) unless length($secret) >= 32; | |
| 60 | ||
| 61 | my $body_hash = sha256_hex($body_txt); | |
| 62 | my $expected = hmac_sha256_hex("$ts.$thread_id.$body_hash", $secret); | |
| 63 | ||
| 64 | # Constant-time compare | |
| 65 | my $ok = length($sig_hdr) == length($expected); | |
| 66 | my $r = 0; | |
| 67 | if ($ok) { | |
| 68 | for (my $i = 0; $i < length($sig_hdr); $i++) { | |
| 69 | $r |= ord(substr($sig_hdr, $i, 1)) ^ ord(substr($expected, $i, 1)); | |
| 70 | } | |
| 71 | $ok = ($r == 0); | |
| 72 | } | |
| 73 | _json_out(403, { error => 'bad_signature' }) unless $ok; | |
| 74 | ||
| 75 | # Signature valid -- mirror PT's handle_reply DB mutations. | |
| 76 | my $db = MODS::DBConnect->new; | |
| 77 | my $dbh = $db->db_connect(); | |
| 78 | _json_out(500, { error => 'db_connect_failed' }) unless $dbh; | |
| 79 | my $DB = $cfg->settings('database_name'); | |
| 80 | ||
| 81 | # Quote body safely via $dbh->quote (per rules: prefer $dbh->quote over s/'/''/g). | |
| 82 | my $body_q = $dbh->quote($body_txt); | |
| 83 | # PT's sender_user_id is NOT NULL (differs from 6 sister sites). Use | |
| 84 | # user_id=1 (Shawn -- Platform Owner) as the portfolio operator on PT. | |
| 85 | my $admin_uid = 1; | |
| 86 | $db->db_readwrite($dbh, qq~ | |
| 87 | INSERT INTO `${DB}`.support_messages | |
| 88 | (thread_id, sender_user_id, sender_kind, body) | |
| 89 | VALUES ($thread_id, $admin_uid, 'admin', $body_q) | |
| 90 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 91 | ||
| 92 | $db->db_readwrite($dbh, qq~ | |
| 93 | UPDATE `${DB}`.support_threads | |
| 94 | SET status = 'answered', | |
| 95 | user_unread = 1, | |
| 96 | last_message_by = 'admin', | |
| 97 | last_message_at = NOW() | |
| 98 | WHERE id = $thread_id | |
| 99 | LIMIT 1 | |
| 100 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 101 | ||
| 102 | my $mid_row = $db->db_readwrite($dbh, qq~ | |
| 103 | SELECT id FROM `${DB}`.support_messages | |
| 104 | WHERE thread_id = $thread_id | |
| 105 | ORDER BY id DESC LIMIT 1 | |
| 106 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 107 | my $mid = ($mid_row && $mid_row->{id}) ? $mid_row->{id} : 0; | |
| 108 | ||
| 109 | $db->db_disconnect($dbh); | |
| 110 | _json_out(200, { ok => 1, message_id => $mid }); |