Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/forms.cgi
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/forms.cgi

added on local at 2026-07-01 15:02:45

Added
+343
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 1b56310fcc99
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# ContactForge -- /forms.cgi
4#
5# Manage public lead-capture forms. List view + builder side panel.
6# Public URLs land at /form_render.cgi?t=<token> and submit to
7# /form_submit.cgi.
8#
9# Consolidates the former /form_action.cgi (POST handler) via
10# SCRIPT_NAME dispatch. The _action.cgi file is a wrapper that
11# `do`s this file.
12#======================================================================
13use strict;
14use warnings;
15
16use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com';
17use CGI;
18use JSON::PP;
19use MODS::Template;
20use MODS::DBConnect;
21use MODS::Login;
22use MODS::ContactForge::Config;
23use MODS::ContactForge::Wrapper;
24
25my $q = CGI->new;
26my $auth = MODS::Login->new;
27my $wrap = MODS::ContactForge::Wrapper->new;
28my $db = MODS::DBConnect->new;
29my $cfg = MODS::ContactForge::Config->new;
30my $DB = $cfg->settings('database_name');
31
32$| = 1;
33my $userinfo = $auth->login_verify();
34if (!$userinfo) {
35 print "Status: 302 Found\nLocation: /login.cgi\n\n";
36 exit;
37}
38my $uid = $userinfo->{user_id};
39$uid =~ s/[^0-9]//g;
40
41my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'forms';
42
43if ($entry eq 'form_action') {
44 _handle_action();
45 exit;
46}
47
48my $dbh = $db->db_connect();
49
50# Pull lead_source list for the auto_source dropdown.
51my @sources = $db->db_readwrite_multiple($dbh, qq~
52 SELECT slug, name FROM `${DB}`.lead_sources ORDER BY name ASC
53~, $ENV{SCRIPT_NAME}, __LINE__);
54
55# Pull existing forms.
56my @forms = $db->db_readwrite_multiple($dbh, qq~
57 SELECT id, token, name, description, success_url, success_message,
58 auto_tag, auto_source_slug, submissions_count, is_active,
59 DATE_FORMAT(created_at, '%b %e, %Y') AS created_pretty,
60 (SELECT DATE_FORMAT(MAX(created_at),'%b %e, %Y %l:%i %p')
61 FROM `${DB}`.lead_form_submissions s
62 WHERE s.form_id=lead_forms.id) AS last_sub_pretty
63 FROM `${DB}`.lead_forms
64 WHERE owner_user_id='$uid'
65 ORDER BY id DESC
66~, $ENV{SCRIPT_NAME}, __LINE__);
67
68$db->db_disconnect($dbh);
69
70sub _h {
71 my $s = shift;
72 $s = '' unless defined $s;
73 $s =~ s/&/&amp;/g;
74 $s =~ s/</&lt;/g;
75 $s =~ s/>/&gt;/g;
76 $s =~ s/"/&quot;/g;
77 return $s;
78}
79
80my $edit_id = $q->param('edit') || '';
81$edit_id =~ s/[^0-9]//g;
82my $is_new = $q->param('new') ? 1 : 0;
83
84# Build the form list HTML.
85my $list_html = '';
86if (@forms) {
87 $list_html .= qq~
88<div class="table-wrap">
89 <table class="table">
90 <thead><tr>
91 <th>Name</th><th>Public URL</th><th>Submissions</th><th>Last submission</th>
92 <th>Active</th><th>Actions</th>
93 </tr></thead>
94 <tbody>
95~;
96 foreach my $f (@forms) {
97 my $url = "/form_render.cgi?t=" . _h($f->{token});
98 my $active_label = $f->{is_active} ? 'On' : 'Off';
99 my $active_color = $f->{is_active} ? '#0d9488' : '#666';
100 $list_html .= qq~<tr>
101 <td><strong>~ . _h($f->{name}) . qq~</strong><div class="text-xs text-dim">~ . _h($f->{description}) . qq~</div></td>
102 <td><a href="$url" target="_blank" class="font-mono text-xs">$url</a></td>
103 <td>$f->{submissions_count}</td>
104 <td class="text-xs text-dim">~ . _h($f->{last_sub_pretty} || '-') . qq~</td>
105 <td><span class="pill" style="color:$active_color">$active_label</span></td>
106 <td>
107 <a href="/forms.cgi?edit=$f->{id}" class="btn btn-secondary btn-sm">Edit</a>
108 <form method="POST" action="/form_action.cgi" style="display:inline">
109 <input type="hidden" name="op" value="toggle">
110 <input type="hidden" name="id" value="$f->{id}">
111 <button class="btn btn-secondary btn-sm" type="submit">~ . ($f->{is_active} ? 'Disable' : 'Enable') . qq~</button>
112 </form>
113 <form method="POST" action="/form_action.cgi" style="display:inline"
114 onsubmit="return confirm('Delete this form? Past submissions stay in the database.')">
115 <input type="hidden" name="op" value="delete">
116 <input type="hidden" name="id" value="$f->{id}">
117 <button class="btn btn-danger btn-sm" type="submit">Delete</button>
118 </form>
119 </td>
120 </tr>~;
121 }
122 $list_html .= qq~</tbody></table></div>~;
123}
124else {
125 $list_html = qq~<div class="text-dim text-sm">No lead forms yet. Build one and share the public URL on your website, in your email signature, or anywhere else.</div>~;
126}
127
128# Build the editor panel if ?new=1 or ?edit=N.
129my $editor_html = '';
130my $editor_form;
131if ($is_new) {
132 $editor_form = {
133 id => '',
134 name => '',
135 description => '',
136 success_url => '',
137 success_message => 'Thanks - we have your details and will be in touch.',
138 auto_tag => '',
139 auto_source_slug=> 'website',
140 fields_json => '[{"name":"email","label":"Email","type":"email","required":1},'
141 . '{"name":"first_name","label":"First name","type":"text","required":0},'
142 . '{"name":"last_name","label":"Last name","type":"text","required":0}]',
143 };
144}
145elsif ($edit_id) {
146 $dbh = $db->db_connect();
147 my $r = $db->db_readwrite($dbh, qq~
148 SELECT id, name, description, success_url, success_message,
149 auto_tag, auto_source_slug, fields_json
150 FROM `${DB}`.lead_forms
151 WHERE id='$edit_id' AND owner_user_id='$uid' LIMIT 1
152 ~, $ENV{SCRIPT_NAME}, __LINE__);
153 $db->db_disconnect($dbh);
154 $editor_form = $r if $r && $r->{id};
155}
156
157if ($editor_form) {
158 my $src_opts = '';
159 foreach my $s (@sources) {
160 my $slug = _h($s->{slug});
161 my $name = _h($s->{name});
162 my $sel = ($s->{slug} eq ($editor_form->{auto_source_slug} || '')) ? ' selected' : '';
163 $src_opts .= qq~<option value="$slug"$sel>$name</option>~;
164 }
165 my $title = $editor_form->{id} ? 'Edit form' : 'New form';
166 $editor_html = qq~
167<div class="card" style="margin-top:24px">
168 <h3 style="margin-top:0">$title</h3>
169 <form method="POST" action="/form_action.cgi" style="display:grid;grid-template-columns:repeat(2,1fr);gap:14px">
170 <input type="hidden" name="op" value="save">
171 <input type="hidden" name="id" value="~ . _h($editor_form->{id}) . qq~">
172 <div>
173 <label class="text-xs text-dim">Form name (internal)</label>
174 <input type="text" name="name" class="input" required value="~ . _h($editor_form->{name}) . qq~" placeholder="e.g. \"Homepage demo request\"">
175 </div>
176 <div>
177 <label class="text-xs text-dim">Lead source</label>
178 <select name="auto_source_slug" class="input">$src_opts</select>
179 </div>
180 <div style="grid-column:1/-1">
181 <label class="text-xs text-dim">Description (shown above the form to visitors)</label>
182 <input type="text" name="description" class="input" value="~ . _h($editor_form->{description}) . qq~">
183 </div>
184 <div>
185 <label class="text-xs text-dim">Auto-tag every lead with</label>
186 <input type="text" name="auto_tag" class="input" value="~ . _h($editor_form->{auto_tag}) . qq~" placeholder="e.g. \"website lead\"">
187 </div>
188 <div>
189 <label class="text-xs text-dim">Success URL (blank = generic thank-you page)</label>
190 <input type="text" name="success_url" class="input" value="~ . _h($editor_form->{success_url}) . qq~" placeholder="https://yoursite.com/thanks">
191 </div>
192 <div style="grid-column:1/-1">
193 <label class="text-xs text-dim">Success message</label>
194 <input type="text" name="success_message" class="input" value="~ . _h($editor_form->{success_message}) . qq~">
195 </div>
196 <div style="grid-column:1/-1">
197 <label class="text-xs text-dim">Fields (JSON array of {name,label,type,required})</label>
198 <textarea name="fields_json" rows="6" class="input" style="font-family:monospace">~ . _h($editor_form->{fields_json}) . qq~</textarea>
199 <div class="text-xs text-dim" style="margin-top:6px">type can be text / email / tel / textarea / select. For select, also pass an "options" array.</div>
200 </div>
201 <div style="grid-column:1/-1;display:flex;gap:10px">
202 <button class="btn btn-primary" type="submit">Save form</button>
203 <a href="/forms.cgi" class="btn btn-secondary">Cancel</a>
204 </div>
205 </form>
206</div>
207~;
208}
209
210my $body = qq~
211<div class="page-shell" style="max-width:1100px">
212 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px">
213 <h1 style="margin:0">Lead forms</h1>
214 <a href="/forms.cgi?new=1" class="btn btn-primary">+ New form</a>
215 </div>
216 <p class="text-dim">Build a hosted form and embed the link anywhere. Submissions land in your Contacts list, dedupe by email, and (optionally) get a tag for tracking.</p>
217 <div class="card">
218 $list_html
219 </div>
220 $editor_html
221</div>
222~;
223
224print "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";
225
226$wrap->render({
227 userinfo => $userinfo,
228 page_key => 'forms',
229 title => 'Lead forms',
230 body => $body,
231});
232
233#======================================================================
234# form_action entry: POST handler for create/update/delete/toggle on lead_forms.
235#======================================================================
236sub _act_esc {
237 my $s = shift;
238 $s = '' unless defined $s;
239 $s =~ s/\\/\\\\/g;
240 $s =~ s/'/''/g;
241 return $s;
242}
243
244sub _act_make_token {
245 # 32-hex pseudo-UUID.
246 my @hex = ('0'..'9','a'..'f');
247 my $t = '';
248 $t .= $hex[int(rand(16))] for (1..32);
249 return $t;
250}
251
252sub _handle_action {
253 my $op = lc($q->param('op') || '');
254 my $id = $q->param('id') || '';
255 $id =~ s/[^0-9]//g;
256
257 my $dbh = $db->db_connect();
258
259 if ($op eq 'save') {
260 my $name = $q->param('name') // '';
261 my $desc = $q->param('description') // '';
262 my $surl = $q->param('success_url') // '';
263 my $smsg = $q->param('success_message') // '';
264 my $tag = $q->param('auto_tag') // '';
265 my $src = $q->param('auto_source_slug') // '';
266 my $fields = $q->param('fields_json') // '[]';
267
268 # Cheap validation -- try to round-trip the JSON. If it fails, fall
269 # back to an empty array so we never write garbage.
270 my $ok = eval { JSON::PP::decode_json($fields); 1; };
271 $fields = '[]' unless $ok;
272
273 for ($name, $desc, $surl, $smsg, $tag, $src) {
274 s/^\s+|\s+$//g if defined;
275 }
276
277 if ($name eq '') {
278 $db->db_disconnect($dbh);
279 print "Status: 302 Found\nLocation: /forms.cgi?edit=$id&flash=missing\n\n";
280 return;
281 }
282
283 if ($id) {
284 $db->db_readwrite($dbh, qq~
285 UPDATE `${DB}`.lead_forms
286 SET name='~ . _act_esc($name) . qq~',
287 description='~ . _act_esc($desc) . qq~',
288 success_url='~ . _act_esc($surl) . qq~',
289 success_message='~ . _act_esc($smsg) . qq~',
290 auto_tag='~ . _act_esc($tag) . qq~',
291 auto_source_slug='~. _act_esc($src) . qq~',
292 fields_json='~ . _act_esc($fields) . qq~',
293 updated_at=NOW()
294 WHERE id='$id' AND owner_user_id='$uid'
295 ~, $ENV{SCRIPT_NAME}, __LINE__);
296 } else {
297 my $token = _act_make_token();
298 my $ins = $db->db_readwrite($dbh, qq~
299 INSERT INTO `${DB}`.lead_forms
300 SET owner_user_id='$uid',
301 token='~ . _act_esc($token) . qq~',
302 name='~ . _act_esc($name) . qq~',
303 description='~ . _act_esc($desc) . qq~',
304 success_url='~ . _act_esc($surl) . qq~',
305 success_message='~ . _act_esc($smsg) . qq~',
306 auto_tag='~ . _act_esc($tag) . qq~',
307 auto_source_slug='~ . _act_esc($src) . qq~',
308 fields_json='~ . _act_esc($fields) . qq~',
309 is_active='1'
310 ~, $ENV{SCRIPT_NAME}, __LINE__);
311 $id = $ins->{mysql_insertid} || 0;
312 }
313 $db->db_disconnect($dbh);
314 print "Status: 302 Found\nLocation: /forms.cgi?flash=saved\n\n";
315 return;
316 }
317
318 if ($op eq 'toggle' && $id) {
319 $db->db_readwrite($dbh, qq~
320 UPDATE `${DB}`.lead_forms
321 SET is_active = 1 - is_active,
322 updated_at=NOW()
323 WHERE id='$id' AND owner_user_id='$uid'
324 ~, $ENV{SCRIPT_NAME}, __LINE__);
325 $db->db_disconnect($dbh);
326 print "Status: 302 Found\nLocation: /forms.cgi\n\n";
327 return;
328 }
329
330 if ($op eq 'delete' && $id) {
331 $db->db_readwrite($dbh, qq~
332 DELETE FROM `${DB}`.lead_forms
333 WHERE id='$id' AND owner_user_id='$uid'
334 ~, $ENV{SCRIPT_NAME}, __LINE__);
335 $db->db_disconnect($dbh);
336 print "Status: 302 Found\nLocation: /forms.cgi?flash=deleted\n\n";
337 return;
338 }
339
340 $db->db_disconnect($dbh);
341 print "Status: 302 Found\nLocation: /forms.cgi\n\n";
342 return;
343}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help