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

O Operator
Diff

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

added on local at 2026-07-01 22:10:07

Added
+225
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 2094e9117b93
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 -- update a single field on a model row.
4#
5# POST params:
6# field = one of: title, price, tagline, description,
7# hero_image_url, hero_image_pos
8# value = new value (string)
9# model_id = which model to update
10#
11# Used by the in-place storefront editor for the FEATURED product
12# (model-level fields like title and price), as a sibling to
13# update_storefront_field.cgi which handles storefront-level fields.
14#
15# Authorization: the model must belong to the logged-in user
16# (models.user_id == userinfo.user_id).
17#
18# Returns JSON { success: 1 } or { success: 0, error: "..." }.
19#======================================================================
20use strict;
21use warnings;
22
23use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
24use CGI;
25use MODS::DBConnect;
26use MODS::Login;
27use MODS::ShopCart::Config;
28
29$| = 1;
30
31my $q = CGI->new;
32my $db = MODS::DBConnect->new;
33my $auth = MODS::Login->new;
34my $config = MODS::ShopCart::Config->new;
35my $DB = $config->settings('database_name');
36
37print "Content-Type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
38
39# DBConnect's error() helper calls exit() on a failing SQL call, which
40# bypasses any eval{} and leaves the response body empty. END block
41# emits a JSON message if that happens so the editor never gets a
42# silent 200/empty.
43our $SAVE_DONE = 0;
44END {
45 if (!$SAVE_DONE) {
46 print qq~{"success":0,"error":"server aborted mid-update -- check MODS/DB_ERRORS.log for the failing SQL and DBI message"}~;
47 }
48}
49
50sub fail {
51 my ($msg) = @_;
52 $msg =~ s/"/\\"/g;
53 print qq~{"success":0,"error":"$msg"}~;
54 $SAVE_DONE = 1;
55 exit;
56}
57
58my $userinfo = $auth->login_verify();
59fail('not authenticated') unless $userinfo && $userinfo->{user_id};
60require MODS::ShopCart::Permissions;
61fail('not authorized') unless MODS::ShopCart::Permissions->new->has_feature($userinfo, 'edit_models');
62
63my $uid = $userinfo->{user_id};
64$uid =~ s/[^0-9]//g;
65fail('bad user id') unless $uid;
66
67# Whitelist: client field name => SQL column. The "price" alias accepts
68# a user-typed dollar amount and we convert it to base_price_cents.
69my %ALLOWED = (
70 title => 'title',
71 price => 'base_price_cents',
72 tagline => 'tagline',
73 description => 'description',
74 category => 'category',
75 license_type => 'license_type',
76 status => 'status',
77 visibility => 'visibility',
78 hero_image_url => 'hero_image_url',
79 hero_image_pos => 'hero_image_pos',
80);
81
82my %MAX_LEN = (
83 title => 200,
84 tagline => 180,
85 description => 32000,
86 category => 80,
87 license_type => 80,
88 hero_image_url => 255,
89 hero_image_pos => 20,
90);
91
92# Enum whitelists for fields that have constrained DB values.
93my %ENUM = (
94 status => { map { $_ => 1 } qw(draft published unlisted archived removed) },
95 visibility => { map { $_ => 1 } qw(public unlisted private) },
96);
97
98my $field = $q->param('field') || '';
99fail('missing field') unless $field;
100fail("field '$field' is not editable") unless exists $ALLOWED{$field};
101my $column = $ALLOWED{$field};
102
103my $model_id = $q->param('model_id') || '';
104$model_id =~ s/[^0-9]//g;
105fail('missing or bad model_id') unless $model_id;
106
107my $value = $q->param('value');
108$value = '' unless defined $value;
109
110# Field-specific parsing.
111my $sql_value;
112if ($field eq 'price') {
113 # Accept "$15", "15.00", "15.5" -> 1500 cents. Strip non-numeric
114 # except for one decimal point.
115 my $raw = $value;
116 $raw =~ s/[^0-9.]//g;
117 my $dollars = $raw + 0;
118 if ($dollars < 0) { $dollars = 0; }
119 my $cents = int($dollars * 100 + 0.5);
120 $sql_value = "'$cents'";
121} elsif ($ENUM{$field}) {
122 # Constrained-value column. Reject anything not in the whitelist.
123 my $v = lc($value); $v =~ s/[^a-z]//g;
124 fail("invalid $field value") unless $ENUM{$field}{$v};
125 $sql_value = "'$v'";
126} else {
127 my $cap = $MAX_LEN{$field} || 255;
128 $value = substr($value, 0, $cap) if length($value) > $cap;
129 my $safe = $value;
130 $safe =~ s/\\/\\\\/g;
131 $safe =~ s/'/\\'/g;
132 $sql_value = "'$safe'";
133}
134
135# Verify the model belongs to this user before writing. Reject writes
136# to permanently-deleted (purged) models -- those are visible only to
137# admin tooling; the user can't reach them anymore.
138my $dbh = $db->db_connect();
139my $owner = $db->db_readwrite($dbh,
140 qq~SELECT user_id, purged_at FROM `${DB}`.models WHERE id='$model_id' LIMIT 1~,
141 $ENV{SCRIPT_NAME}, __LINE__);
142unless ($owner && $owner->{user_id} && $owner->{user_id} eq $uid) {
143 $db->db_disconnect($dbh);
144 fail('model not found or not yours');
145}
146if ($owner->{purged_at}) {
147 $db->db_disconnect($dbh);
148 fail('model has been permanently deleted');
149}
150
151# Wrap UPDATE so a missing column gets reported back via the END block.
152my $err;
153eval {
154 $db->db_readwrite($dbh, qq~
155 UPDATE `${DB}`.models
156 SET `$column` = $sql_value, updated_at = NOW()
157 WHERE id = '$model_id' LIMIT 1
158 ~, $ENV{SCRIPT_NAME}, __LINE__);
159 1;
160} or do { $err = $@ || 'unknown SQL error'; };
161
162# When the seller flips status to 'published' from this endpoint
163# (the My Listings inline PUBLISH button), we also have to insert a
164# storefront_listings row -- otherwise the model is "published" in
165# the DB but never shows on the seller's storefront because the
166# storefront query joins storefront_listings WHERE visible=1.
167# upload.cgi has the equivalent auto-list block for the form-driven
168# publish path; this mirrors it for the AJAX path so the two flows
169# stay in sync. INSERT IGNORE makes it safe to call repeatedly --
170# the unique key on (storefront_id, model_id) absorbs duplicates,
171# so an already-listed model just gets a no-op.
172if (!$err && $field eq 'status' && $value && lc($value) eq 'published') {
173 eval {
174 my $sf = $db->db_readwrite($dbh,
175 qq~SELECT id FROM `${DB}`.storefronts WHERE user_id='$uid' LIMIT 1~,
176 $ENV{SCRIPT_NAME}, __LINE__);
177 if ($sf && $sf->{id}) {
178 my $sid = $sf->{id}; $sid =~ s/[^0-9]//g;
179 # Put the newly-published model at the top of the
180 # storefront order. Mirrors upload.cgi -- new published
181 # work is what the seller wants the buyer to see first.
182 my $top = $db->db_readwrite($dbh, qq~
183 SELECT COALESCE(MIN(sort_order), 0) - 1 AS new_top
184 FROM `${DB}`.storefront_listings
185 WHERE storefront_id='$sid'
186 ~, $ENV{SCRIPT_NAME}, __LINE__);
187 my $top_sort = ($top && defined $top->{new_top}) ? $top->{new_top} : 0;
188 $db->db_readwrite($dbh, qq~
189 INSERT IGNORE INTO `${DB}`.storefront_listings SET
190 storefront_id = '$sid',
191 model_id = '$model_id',
192 visible = 1,
193 override_price_cents = NULL,
194 sort_order = '$top_sort',
195 added_at = NOW()
196 ~, $ENV{SCRIPT_NAME}, __LINE__);
197 }
198 1;
199 } or do {
200 # Auto-listing failed but the status update itself succeeded.
201 # Don't surface the secondary failure -- the seller can still
202 # toggle visibility manually from My Listings. Just log via
203 # the END block path so we know what happened.
204 $err = "status updated but auto-list failed: " . ($@ || 'unknown');
205 };
206}
207
208$db->db_disconnect($dbh);
209
210if ($err) {
211 my $msg = $err;
212 $msg =~ s/[\r\n]+/ /g;
213 $msg =~ s/"/\\"/g;
214 my $hint = '';
215 if ($msg =~ /Unknown column/i) {
216 $hint = " (the column may not exist yet -- run the models ALTER from installation_instructions.html)";
217 }
218 print qq~{"success":0,"error":"SQL error: $msg$hint"}~;
219 $SAVE_DONE = 1;
220 exit;
221}
222
223print qq~{"success":1,"field":"$field","model_id":"$model_id"}~;
224$SAVE_DONE = 1;
225exit;