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

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

added on local at 2026-07-01 12:34:13

Added
+562
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 773e36dc9cf3
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##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26use CGI; my $query = new CGI; my $form = $query->Vars;
27use MODS::Login; my $login = new MODS::Login;
28use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls();
29use MODS::Template; my $tfile = new MODS::Template;
30use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
31use MODS::DBConnect; my $db = new MODS::DBConnect;
32use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name');
33use MODS::PTMatrix::PM;
34use MODS::PTMatrix::CustomFields;
35
36
37
38#############################################################
39## Get form data
40#############################################################
41my %rawform = %$form;
42
43foreach my $line(keys %$form){
44 $form->{$line} =~ s/[^!-~\s]//g;
45 $form->{$line} = quotemeta("$form->{$line}");
46}
47
48
49
50############################################################
51# Program Controls
52############################################################
53$|=1;
54my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'forms';
55
56if($entry eq 'form'){
57 my $slug = $rawform{slug} || '';
58 $slug =~ s/[^a-z0-9\-]//g;
59 $slug = substr($slug, 0, 80);
60 if(!$slug){&render_404;}
61 &public_form($slug);
62 exit;
63}
64
65my $userinfo = $login->login_verify();
66print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
67if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;}
68elsif(!MODS::PTMatrix::PM::get_company_id($userinfo)){print qq~<script>window.location="$url->{admin_companies}";</script>~;}
69elsif(!MODS::PTMatrix::PM::is_manager_or_admin($userinfo)){print qq~<script>window.location="$url->{dashboard}";</script>~;}
70elsif(($ENV{REQUEST_METHOD}||'') eq 'POST'){&form_action;}
71else{&forms_list;}
72
73
74############################################################
75# Subroutines
76############################################################
77sub slugify{
78 my($s) = @_;
79 $s = lc($s);
80 $s =~ s/[^a-z0-9]+/-/g;
81 $s =~ s/^-+|-+$//g;
82 $s = substr($s, 0, 60);
83 return $s || 'form';
84}
85
86#Convert the 12 slot fields (fN_label, fN_type, fN_required, fN_options, fN_cf_id) into JSON
87sub build_fields_json{
88 my @out;
89 for(my $i = 1; $i <= 12; $i++){
90 my $lbl = $rawform{"f${i}_label"};
91 next unless defined($lbl) && length($lbl);
92 my $type = $form->{"f${i}_type"} || 'text';
93 $type = 'text' unless $type =~ /^(text|textarea|email|number|date|select|checkbox)$/;
94 my $req = $form->{"f${i}_required"} ? 1 : 0;
95 my $opts_raw = $rawform{"f${i}_options"} || '';
96 my @opts;
97 if($type eq 'select'){
98 @opts = grep { length } map { my $s = $_; $s =~ s/^\s+|\s+$//g; $s } split(/\r?\n/, $opts_raw);
99 }
100
101 #Optional cf_id pulls the submitted answer into a PTMatrix custom field on the resulting ticket
102 my $cf = $form->{"f${i}_cf_id"} || '';
103 $cf =~ s/[^0-9]//g;
104 $cf = 0 unless length $cf;
105 $cf += 0;
106
107 my $j_label = $lbl; $j_label =~ s/\\/\\\\/g; $j_label =~ s/"/\\"/g;
108 my $opts_str = '[' . join(',', map { my $o = $_; $o =~ s/\\/\\\\/g; $o =~ s/"/\\"/g; qq~"$o"~ } @opts) . ']';
109 push @out, sprintf('{"label":"%s","type":"%s","required":%d,"options":%s,"cf_id":%d}', $j_label, $type, $req, $opts_str, $cf);
110 }
111 return '[' . join(',', @out) . ']';
112}
113
114#Parse stored fields JSON back into per-slot hashrefs (legacy shape + new cf_id shape both handled)
115sub parse_fields_json{
116 my($json) = @_;
117 my @out;
118 return @out unless $json && $json =~ /^\s*\[/;
119
120 while($json =~ /\{\s*"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"type"\s*:\s*"([^"]+)"\s*,\s*"required"\s*:\s*([01])\s*,\s*"options"\s*:\s*\[([^\]]*)\]\s*(?:,\s*"cf_id"\s*:\s*(\d+))?\s*\}/g){
121 my($label, $type, $req, $opts_raw, $cf_id) = ($1, $2, $3, $4, $5);
122 $label =~ s/\\"/"/g;
123 $label =~ s/\\\\/\\/g;
124 my @opts;
125 while($opts_raw =~ /"((?:[^"\\]|\\.)*)"/g){
126 my $o = $1;
127 $o =~ s/\\"/"/g;
128 $o =~ s/\\\\/\\/g;
129 push @opts, $o;
130 }
131 push @out, {
132 label => $label,
133 type => $type,
134 required => $req + 0,
135 options => \@opts,
136 cf_id => ($cf_id ? $cf_id + 0 : 0),
137 };
138 }
139 return @out;
140}
141
142sub form_action{
143 my $cid = MODS::PTMatrix::PM::get_company_id($userinfo);
144 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
145 my $act = $form->{act} || '';
146 my $flash = '';
147
148 my $dbh = $db->db_connect();
149
150 if($act eq 'save'){
151 my $id = MODS::PTMatrix::PM::safe_int($form->{id});
152 my $name = $rawform{name} || '';
153 my $desc = $rawform{description} || '';
154 my $pid = MODS::PTMatrix::PM::safe_int($form->{project_id});
155 my $submit_label = $rawform{submit_label} || 'Submit';
156 my $success_message = $rawform{success_message} || 'Thanks! We received your submission.';
157 my $prio = ($form->{default_priority} =~ /^(low|medium|high|urgent|critical)$/) ? $form->{default_priority} : 'medium';
158 my $slug = $rawform{slug} || &slugify($name);
159 $slug = &slugify($slug);
160 my $is_active = $form->{is_active} ? 1 : 0;
161 my $fields_json = &build_fields_json;
162
163 #Confirm the target project belongs to this company
164 my $pcheck = $db->db_readwrite($dbh, qq~SELECT id FROM `${database_name}`.projects WHERE id='$pid' AND company_id='$cid' LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__);
165 if(!$pcheck || !$pcheck->{id}){
166 $flash = 'Pick a valid project.';
167 }elsif(!length($name)){
168 $flash = 'Form name is required.';
169 }else{
170 my $name_q = MODS::PTMatrix::PM::sql_quote(substr($name, 0, 120));
171 my $desc_q = MODS::PTMatrix::PM::sql_quote(substr($desc, 0, 500));
172 my $slug_q = MODS::PTMatrix::PM::sql_quote(substr($slug, 0, 80));
173 my $submit_q = MODS::PTMatrix::PM::sql_quote(substr($submit_label, 0, 60));
174 my $succ_q = MODS::PTMatrix::PM::sql_quote(substr($success_message, 0, 500));
175 my $fields_q = MODS::PTMatrix::PM::sql_quote($fields_json);
176
177 if($id){
178 my $sql = qq~UPDATE `${database_name}`.forms SET name='$name_q', description='$desc_q', slug='$slug_q', project_id='$pid', submit_label='$submit_q', success_message='$succ_q', default_priority='$prio', fields_json='$fields_q', is_active='$is_active' WHERE id='$id' AND company_id='$cid'~;
179 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
180 $flash = "Form <strong>$name_q</strong> saved.";
181 }else{
182 my $sql = qq~INSERT INTO `${database_name}`.forms (company_id, project_id, name, description, slug, submit_label, success_message, default_priority, fields_json, is_active, created_by_user_id, created_at) VALUES ('$cid', '$pid', '$name_q', '$desc_q', '$slug_q', '$submit_q', '$succ_q', '$prio', '$fields_q', '$is_active', '$uid', NOW())~;
183 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
184 $flash = "Form <strong>$name_q</strong> created.";
185 }
186 }
187 }elsif($act eq 'toggle_active'){
188 my $id = MODS::PTMatrix::PM::safe_int($form->{id});
189 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.forms SET is_active=1-is_active WHERE id='$id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
190 $flash = 'Form toggled.';
191 }elsif($act eq 'delete'){
192 my $id = MODS::PTMatrix::PM::safe_int($form->{id});
193 $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.forms WHERE id='$id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
194 $flash = 'Form deleted.';
195 }
196
197 $db->db_disconnect($dbh);
198 &forms_list($flash);
199}
200
201sub forms_list{
202 my($flash) = @_;
203 $flash ||= '';
204
205 my $cid = MODS::PTMatrix::PM::get_company_id($userinfo);
206 my $dbh = $db->db_connect();
207
208 #------------------------------------------------------------------------------------------------------
209 #Custom fields available for the "map to CF" dropdown
210 my $cf_mod = MODS::PTMatrix::CustomFields->new;
211 my @company_cfs;
212 if($cf_mod->schema_ready($db, $dbh, $database_name)){
213 @company_cfs = $cf_mod->list_definitions($db, $dbh, $database_name, $cid, 1);
214 }
215
216 #Active projects for the project picker
217 my @all_projects = $db->db_readwrite_multiple($dbh, qq~SELECT id, name, key_prefix FROM `${database_name}`.projects WHERE company_id='$cid' AND status IN ('active','planning') ORDER BY name~, $ENV{SCRIPT_NAME}, __LINE__);
218 foreach my $p(@all_projects){$p->{display} = MODS::PTMatrix::PM::h("$p->{key_prefix} &mdash; $p->{name}");}
219
220 #All forms this company has
221 my $sql_f = qq~SELECT f.id, f.name, f.description, f.slug, f.project_id, f.submit_label, f.success_message, f.default_priority, f.fields_json, f.is_active, f.submission_count, f.created_at, p.name AS project_name, p.key_prefix FROM `${database_name}`.forms f LEFT JOIN `${database_name}`.projects p ON p.id = f.project_id WHERE f.company_id='$cid' ORDER BY f.is_active DESC, f.id DESC~;
222 my @forms = $db->db_readwrite_multiple($dbh, $sql_f, $ENV{SCRIPT_NAME}, __LINE__);
223 #------------------------------------------------------------------------------------------------------
224
225 $db->db_disconnect($dbh);
226
227 #Build the per-slot CF dropdown once - stamp selected="" per slot below
228 my $cf_options_base = '<option value="0">&mdash; don\'t map &mdash;</option>';
229 foreach my $d(@company_cfs){
230 my $label = MODS::PTMatrix::PM::h($d->{label});
231 $cf_options_base .= qq~<option value="$d->{id}" data-cf-id="$d->{id}">$label</option>~;
232 }
233
234 foreach my $f(@forms){
235 $f->{name} = MODS::PTMatrix::PM::h($f->{name});
236 $f->{description} = MODS::PTMatrix::PM::h($f->{description} || '');
237 $f->{slug} = MODS::PTMatrix::PM::h($f->{slug});
238 $f->{submit_label} = MODS::PTMatrix::PM::h($f->{submit_label});
239 $f->{success_message} = MODS::PTMatrix::PM::h($f->{success_message});
240 $f->{project_display} = MODS::PTMatrix::PM::h(($f->{key_prefix} || '') . " " . ($f->{project_name} || ''));
241 $f->{public_url} = "https://ptmatrix.3dshawn.com/form.cgi?slug=$f->{slug}";
242 $f->{public_url_disp} = $f->{public_url};
243 $f->{toggle_label} = $f->{is_active} ? 'Disable' : 'Enable';
244 $f->{active_attr} = $f->{is_active} ? 'checked' : '';
245 for my $p(qw(low medium high urgent critical)){$f->{"prio_$p"} = ($f->{default_priority} eq $p) ? 'selected' : '';}
246
247 #Per-form project select markup
248 my @opts;
249 for my $proj(@all_projects){
250 my $sel = ($proj->{id} == $f->{project_id}) ? ' selected' : '';
251 push @opts, qq~<option value="$proj->{id}"$sel>$proj->{display}</option>~;
252 }
253 $f->{project_options_html} = join('', @opts);
254
255 #Parse the stored fields into 12 slots
256 my @fields = &parse_fields_json($f->{fields_json} || '');
257 my @slots;
258 for(my $i = 1; $i <= 12; $i++){
259 my $fld = $fields[$i-1] || { label => '', type => 'text', required => 0, options => [], cf_id => 0 };
260 for my $t(qw(text textarea email number date select checkbox)){
261 $fld->{"sel_$t"} = ($fld->{type} eq $t) ? 'selected' : '';
262 }
263 $fld->{label_h} = MODS::PTMatrix::PM::h($fld->{label});
264 $fld->{options_h} = MODS::PTMatrix::PM::h(join("\n", @{ $fld->{options} || [] }));
265 $fld->{required_attr} = $fld->{required} ? 'checked' : '';
266 $fld->{slot} = $i;
267 $fld->{cf_id} = ($fld->{cf_id} || 0) + 0;
268
269 #Stamp selected on the matching CF option
270 my $opts_html = $cf_options_base;
271 if($fld->{cf_id}){
272 $opts_html =~ s/value="$fld->{cf_id}"/value="$fld->{cf_id}" selected/;
273 }else{
274 $opts_html =~ s/value="0">/value="0" selected>/;
275 }
276 $fld->{cf_options_html} = $opts_html;
277 push @slots, $fld;
278 }
279 $f->{slots} = \@slots;
280 }
281
282 #Empty-slot template for the "new form" panel
283 my @empty_slots;
284 my $cf_options_unset = $cf_options_base;
285 $cf_options_unset =~ s/value="0">/value="0" selected>/;
286 for(my $i = 1; $i <= 12; $i++){
287 push @empty_slots, {
288 slot => $i,
289 label_h => '',
290 options_h => '',
291 required_attr => '',
292 cf_id => 0,
293 cf_options_html => $cf_options_unset,
294 map { ("sel_$_" => ($_ eq 'text' ? 'selected' : '')) } qw(text textarea email number date select checkbox),
295 };
296 }
297
298 my @new_options;
299 for my $proj(@all_projects){push @new_options, qq~<option value="$proj->{id}">$proj->{display}</option>~;}
300 my $new_project_options_html = '<option value="">&mdash; pick a project &mdash;</option>' . join('', @new_options);
301
302 #Create all template variables
303 my $tvars;
304 $tvars->{flash} = $flash;
305 $tvars->{has_flash} = length($flash) ? 1 : 0;
306 $tvars->{forms} = \@forms;
307 $tvars->{has_forms} = scalar(@forms) ? 1 : 0;
308 $tvars->{new_slots} = \@empty_slots;
309 $tvars->{new_project_options_html} = $new_project_options_html;
310 $tvars->{has_projects} = scalar(@all_projects) ? 1 : 0;
311
312 my $body = join('', $tfile->template('tf_forms.html', $tvars, $userinfo));
313
314 $load->render({
315 userinfo => $userinfo,
316 page_key => 'forms',
317 title => 'Forms',
318 body => $body,
319 });
320}
321
322#============================================================
323# form entry: public form submission page (no login)
324#============================================================
325sub pf_h{
326 my $s = shift // '';
327 $s =~ s/&/&amp;/g;
328 $s =~ s/</&lt;/g;
329 $s =~ s/>/&gt;/g;
330 $s =~ s/"/&quot;/g;
331 $s =~ s/'/&#39;/g;
332 return $s;
333}
334
335sub pf_json_escape{
336 my($s) = @_;
337 $s //= '';
338 $s =~ s/\\/\\\\/g;
339 $s =~ s/"/\\"/g;
340 $s =~ s/\n/\\n/g;
341 $s =~ s/\r//g;
342 $s =~ s/\t/\\t/g;
343 return $s;
344}
345
346sub render_404{
347 print qq~Status: 404 Not Found\nContent-Type: text/html\n\n~;
348 print qq~<!doctype html><html><body style='font-family:sans-serif;text-align:center;padding:80px;background:#0a0d12;color:#fff'><h1>Form not found</h1><p>This form may have been removed or is no longer accepting submissions.</p></body></html>~;
349 exit;
350}
351
352sub honeypot_ok_response{
353 print qq~Status: 200 OK\nContent-Type: text/html\n\n~;
354 print qq~<!doctype html><html><body>OK</body></html>~;
355 exit;
356}
357
358sub process_submission{
359 my($dbh, $f, $fields) = @_;
360
361 my $subj = $rawform{subject} || '';
362 my $email = $rawform{email} || '';
363 $subj =~ s/^\s+|\s+$//g;
364 $email =~ s/^\s+|\s+$//g;
365
366 if(!length($subj) || !length($email)){return ('', 'Please fill in Subject and Email.');}
367
368 my $missing = '';
369 for(my $i = 0; $i < @$fields; $i++){
370 my $fld = $fields->[$i];
371 next unless $fld->{required};
372 my $key = 'cf' . ($i + 1);
373 my $val = $rawform{$key} // '';
374 $val =~ s/^\s+|\s+$//g;
375 if(!length($val)){$missing = $fld->{label}; last;}
376 }
377 if($missing){return ('', "Please fill in: $missing");}
378
379 my @desc_lines;
380 push @desc_lines, "Submitted via form: $f->{name}";
381 push @desc_lines, "Contact email: $email";
382 push @desc_lines, "";
383 for(my $i = 0; $i < @$fields; $i++){
384 my $fld = $fields->[$i];
385 my $key = 'cf' . ($i + 1);
386 my $val = $rawform{$key} // '';
387 $val =~ s/^\s+|\s+$//g;
388 next unless length $val;
389 push @desc_lines, "$fld->{label}: $val";
390 }
391 my $desc = join("\n", @desc_lines);
392
393 my $cid = $f->{company_id} + 0;
394 my $pid = $f->{proj_id} + 0;
395 my $owner = $f->{owner_user_id} + 0;
396
397 my $col_row = $db->db_readwrite($dbh, qq~SELECT id FROM `${database_name}`.project_kanban_columns WHERE project_id='$pid' ORDER BY is_backlog_column DESC, position ASC LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__);
398 my $col_id = ($col_row && $col_row->{id}) ? $col_row->{id} + 0 : 0;
399 if(!$col_id){return ('', 'The project for this form has no columns yet. Please contact the form owner.');}
400
401 my $tn = $db->db_readwrite($dbh, qq~SELECT next_ticket_number AS n FROM `${database_name}`.projects WHERE id='$pid' LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__);
402 $tn = ($tn && $tn->{n}) ? $tn->{n} + 0 : 1;
403 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.projects SET next_ticket_number=next_ticket_number+1 WHERE id='$pid'~, $ENV{SCRIPT_NAME}, __LINE__);
404
405 my $maxp = $db->db_readwrite($dbh, qq~SELECT COALESCE(MAX(position_in_column),-1)+1 AS p FROM `${database_name}`.tasks WHERE column_id='$col_id'~, $ENV{SCRIPT_NAME}, __LINE__);
406 my $pos = ($maxp && $maxp->{p}) ? $maxp->{p} + 0 : 0;
407
408 my $subj_q = MODS::PTMatrix::PM::sql_quote(substr($subj, 0, 280));
409 my $desc_q = MODS::PTMatrix::PM::sql_quote($desc);
410 my $prio = $f->{default_priority};
411 my $task_id_row = $db->db_readwrite($dbh, qq~INSERT INTO `${database_name}`.tasks SET company_id='$cid', project_id='$pid', ticket_number='$tn', title='$subj_q', description='$desc_q', column_id='$col_id', position_in_column='$pos', status='open', priority='$prio', reporter_user_id='$owner', created_at=NOW()~, $ENV{SCRIPT_NAME}, __LINE__);
412 my $tid = $task_id_row ? ($task_id_row->{mysql_insertid} || 0) : 0;
413
414 my $email_q = MODS::PTMatrix::PM::sql_quote(substr($email, 0, 191));
415 my $ip = $ENV{HTTP_X_FORWARDED_FOR} || $ENV{REMOTE_ADDR} || '';
416 $ip =~ s/,.*$//; $ip =~ s/\s+//g; $ip = substr($ip, 0, 45);
417 my $ip_q = MODS::PTMatrix::PM::sql_quote($ip);
418
419 my @kv;
420 push @kv, qq~"subject":"~ . &pf_json_escape($subj) . qq~"~;
421 push @kv, qq~"email":"~ . &pf_json_escape($email) . qq~"~;
422 for(my $i = 0; $i < @$fields; $i++){
423 my $key = 'cf' . ($i + 1);
424 my $val = $rawform{$key} // '';
425 push @kv, qq~"~ . &pf_json_escape($fields->[$i]{label}) . qq~":"~ . &pf_json_escape($val) . qq~"~;
426 }
427 my $payload = '{' . join(',', @kv) . '}';
428 my $pl_q = MODS::PTMatrix::PM::sql_quote($payload);
429
430 $db->db_readwrite($dbh, qq~INSERT INTO `${database_name}`.form_submissions (form_id, company_id, submitter_email, source_ip, payload_json, created_task_id, submitted_at) VALUES ('$f->{id}', '$cid', '$email_q', '$ip_q', '$pl_q', ~ . ($tid ? "'$tid'" : 'NULL') . qq~, NOW())~, $ENV{SCRIPT_NAME}, __LINE__);
431 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.forms SET submission_count=submission_count+1 WHERE id='$f->{id}'~, $ENV{SCRIPT_NAME}, __LINE__);
432
433 if($tid){
434 my $cf_mod = MODS::PTMatrix::CustomFields->new;
435 if($cf_mod->schema_ready($db, $dbh, $database_name)){
436 for(my $i = 0; $i < @$fields; $i++){
437 my $fld = $fields->[$i];
438 my $cf_id = $fld->{cf_id} || 0;
439 next unless $cf_id;
440 my $key = 'cf' . ($i + 1);
441 my $val = $rawform{$key} // '';
442 $val =~ s/^\s+|\s+$//g;
443 next unless length $val;
444 my $def = $cf_mod->get_definition($db, $dbh, $database_name, $cf_id);
445 next unless $def && $def->{company_id} == $cid;
446 $cf_mod->set_value($db, $dbh, $database_name,
447 task_id => $tid,
448 field_id => $cf_id,
449 value => $val,
450 actor_user_id => $owner,
451 );
452 }
453 }
454 }
455 MODS::PTMatrix::PM::audit($cid, $owner, 'form.submission', 'task', $tid, "via form $f->{slug}");
456
457 return ($f->{success_message} || 'Thanks! We received your submission.', '');
458}
459
460sub render_field{
461 my($fld, $i) = @_;
462 my $key = 'cf' . ($i + 1);
463 my $label = &pf_h($fld->{label}) . ($fld->{required} ? ' <span style="color:#ef4444">*</span>' : '');
464 my $req_attr = $fld->{required} ? ' required' : '';
465 my $sub;
466
467 if($fld->{type} eq 'textarea'){
468 $sub = qq~<textarea class="input" name="$key" rows="4"$req_attr></textarea>~;
469 }elsif($fld->{type} eq 'email'){
470 $sub = qq~<input class="input" type="email" name="$key"$req_attr>~;
471 }elsif($fld->{type} eq 'number'){
472 $sub = qq~<input class="input" type="number" name="$key"$req_attr>~;
473 }elsif($fld->{type} eq 'date'){
474 $sub = qq~<input class="input" type="date" name="$key"$req_attr>~;
475 }elsif($fld->{type} eq 'select'){
476 my $opts = '<option value="">— pick one —</option>';
477 for my $o(@{ $fld->{options} || [] }){
478 my $oh = &pf_h($o);
479 $opts .= qq~<option value="$oh">$oh</option>~;
480 }
481 $sub = qq~<select class="input" name="$key"$req_attr>$opts</select>~;
482 }elsif($fld->{type} eq 'checkbox'){
483 $sub = qq~<label class="checkbox"><input type="checkbox" name="$key" value="1"$req_attr> Yes</label>~;
484 }else{
485 $sub = qq~<input class="input" type="text" name="$key"$req_attr>~;
486 }
487 return qq~<div class="field"><label>$label</label>$sub</div>~;
488}
489
490sub public_form{
491 my($slug) = @_;
492 my $dbh = $db->db_connect();
493 my $slug_q = MODS::PTMatrix::PM::sql_quote($slug);
494 my $f = $db->db_readwrite($dbh, qq~SELECT f.*, p.id AS proj_id, p.next_ticket_number, p.owner_user_id, p.key_prefix, c.name AS company_name FROM `${database_name}`.forms f JOIN `${database_name}`.projects p ON p.id = f.project_id JOIN `${database_name}`.companies c ON c.id = f.company_id WHERE f.slug='$slug_q' AND f.is_active=1 LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__);
495 if(!$f || !$f->{id}){$db->db_disconnect($dbh); &render_404;}
496
497 my $is_post = (($ENV{REQUEST_METHOD} || '') eq 'POST') ? 1 : 0;
498 my @fields = &parse_fields_json($f->{fields_json} || '');
499 my($flash_ok, $flash_err) = ('', '');
500
501 if($is_post){
502 if(length($rawform{__website} || '') > 0){
503 $db->db_disconnect($dbh);
504 &honeypot_ok_response;
505 }
506 ($flash_ok, $flash_err) = &process_submission($dbh, $f, \@fields);
507 }
508
509 $db->db_disconnect($dbh);
510
511 my @field_html;
512 for(my $i = 0; $i < @fields; $i++){
513 push @field_html, &render_field($fields[$i], $i);
514 }
515 my $fields_block = join("\n", @field_html);
516
517 print qq~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~;
518
519 my $brand = &pf_h($config->settings('brand_name') || 'PTMatrix');
520 my $form_name = &pf_h($f->{name});
521 my $form_desc = &pf_h($f->{description} || '');
522 my $submit_label = &pf_h($f->{submit_label});
523 my $success = &pf_h($flash_ok);
524 my $err = &pf_h($flash_err);
525 my $assets_css = $config->settings('assets_css') || '/assets/css';
526
527 print qq~<!doctype html>
528<html lang="en"><head>
529<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
530<title>$form_name &mdash; $brand</title>
531<link rel="stylesheet" href="$assets_css/site.css?v=20260612a">
532</head><body style="background:var(--col-bg-0);color:var(--col-text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px">
533<div style="max-width:560px;width:100%">
534<div style="text-align:center;margin-bottom:24px">
535 <div style="display:inline-flex;align-items:center;gap:10px"><div class="logo" style="width:36px;height:36px;border-radius:8px;background:linear-gradient(135deg,#2c0508,#480c0f);display:grid;place-items:center;color:#fff;font-weight:700">TF</div><div style="font-weight:700;font-size:18px">$brand</div></div>
536</div>
537<div class="card" style="padding:28px">~;
538
539 if(length($flash_ok)){
540 print qq~<div style="text-align:center;padding:30px 0">
541 <div style="font-size:48px;line-height:1;margin-bottom:14px">&#10003;</div>
542 <h2 style="margin:0 0 8px">$success</h2>
543 <p class="text-muted" style="font-size:13px;margin-top:14px">You can close this tab.</p>
544</div>~;
545 }else{
546 print qq~<h2 style="margin-top:0">$form_name</h2>~;
547 print qq~<p class="text-muted" style="font-size:14px">$form_desc</p>~ if length($form_desc);
548 print qq~<div class="flash-msg" style="background:var(--col-warn-soft);color:var(--col-warn);border-left:3px solid var(--col-warn);padding:10px 14px;margin-bottom:14px">$err</div>~ if length($err);
549 print qq~<form method="POST" action="/form.cgi?slug=$slug">
550 <input type="text" name="__website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" aria-hidden="true">
551 <div class="field"><label>Subject <span style="color:#ef4444">*</span></label><input class="input" type="text" name="subject" required maxlength="200"></div>
552 <div class="field"><label>Your email <span style="color:#ef4444">*</span></label><input class="input" type="email" name="email" required></div>
553 $fields_block
554 <button type="submit" class="btn btn-primary btn-lg" style="width:100%;margin-top:8px">$submit_label</button>
555</form>~;
556 }
557
558 print qq~</div>
559<div style="text-align:center;margin-top:18px;font-size:12px;color:var(--col-text-dim)">Powered by <a href="https://ptmatrix.3dshawn.com" style="color:inherit">PTMatrix</a></div>
560</div>
561</body></html>~;
562}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help