Changeset 3433300
- Timestamp:
- 01/06/2026 07:35:03 AM (2 months ago)
- Location:
- griffinforms-form-builder/trunk
- Files:
-
- 16 added
- 19 edited
-
admin/ajax/form.php (modified) (2 diffs)
-
admin/choices/message.php (added)
-
admin/css/griffinforms.css (modified) (1 diff)
-
admin/email_templates (added)
-
admin/email_templates/admin-alert-follow-up.json (added)
-
admin/email_templates/admin-alert-new-submission.json (added)
-
admin/email_templates/autoresponder-appointment.json (added)
-
admin/email_templates/autoresponder-event.json (added)
-
admin/email_templates/autoresponder-support.json (added)
-
admin/email_templates/autoresponder-thank-you.json (added)
-
admin/html/formcontrols/form/entryalerts.php (modified) (1 diff)
-
admin/html/modals/selectitem.php (modified) (4 diffs)
-
admin/html/pages/single/format.php (modified) (2 diffs)
-
admin/html/pages/single/message.php (modified) (2 diffs)
-
admin/js/local/form.php (modified) (1 diff)
-
admin/js/local/message.php (modified) (2 diffs)
-
admin/language/form.php (modified) (1 diff)
-
admin/language/message.php (modified) (1 diff)
-
admin/secure/form.php (modified) (1 diff)
-
admin/sql/message.php (modified) (1 diff)
-
config.php (modified) (1 diff)
-
frontend/actions/postsubmission.php (modified) (8 diffs)
-
griffinforms.php (modified) (1 diff)
-
includes/mergetokens (added)
-
includes/mergetokens/engine.php (added)
-
includes/mergetokens/resolverinterface.php (added)
-
includes/mergetokens/resolvers (added)
-
includes/mergetokens/resolvers/fieldresolver.php (added)
-
includes/mergetokens/resolvers/formresolver.php (added)
-
includes/mergetokens/resolvers/submissionresolver.php (added)
-
includes/mergetokens/tokenparser.php (added)
-
includes/pipelines/handlers/mailqueuehandler.php (modified) (1 diff)
-
includes/pipelines/jobworker.php (modified) (6 diffs)
-
installdata.php (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
griffinforms-form-builder/trunk/admin/ajax/form.php
r3377151 r3433300 105 105 } 106 106 107 public function getFormFields() 108 { 109 check_ajax_referer('get_form_fields', 'nonce'); 110 $this->checkPostData('form_id', 0); 111 112 $form_id = absint(wp_unslash($_POST['form_id'])); 113 if (!$form_id || !$this->sql->itemExists('form', $form_id)) { 114 return $this->returnError(__('Form not found.', 'griffinforms-form-builder')); 115 } 116 117 $fields = $this->sql->fieldsInForm($form_id); 118 if (!is_array($fields)) { 119 return $this->returnError(__('Unable to fetch form fields.', 'griffinforms-form-builder')); 120 } 121 122 $output = []; 123 foreach ($fields as $field) { 124 if (empty($field->id)) { 125 continue; 126 } 127 $output[] = [ 128 'id' => (int) $field->id, 129 'name' => isset($field->name) ? (string) $field->name : '', 130 'heading' => isset($field->heading) ? (string) $field->heading : '', 131 'field_type' => isset($field->field_type) ? (string) $field->field_type : '', 132 'description' => isset($field->description) ? (string) $field->description : '', 133 ]; 134 } 135 136 return $this->returnSuccess(['fields' => $output]); 137 } 138 107 139 /** 108 140 * Handles AJAX request for fetching the number of rules for a given form. … … 132 164 add_action('wp_ajax_checkEmailField', array($this, 'checkEmailField')); 133 165 add_action('wp_ajax_griffinformsGetRulesCount', [$this, 'getRulesCount']); 166 add_action('wp_ajax_griffinforms_get_form_fields', [$this, 'getFormFields']); 134 167 135 168 } -
griffinforms-form-builder/trunk/admin/css/griffinforms.css
r3424123 r3433300 402 402 } 403 403 404 .gf-entryalert-message-preview { 405 background: #f8f9fa; 406 border: 1px dashed #cfd4da; 407 border-radius: 6px; 408 padding: 6px 8px; 409 min-height: 38px; 410 } 411 412 .gf-entryalert-message-name { 413 font-weight: 600; 414 } 415 416 .gf-merge-token-panel { 417 border: 1px solid #dcdcde; 418 border-radius: 6px; 419 padding: 12px; 420 background: #fff; 421 } 422 404 423 .griffinforms-toast { 405 424 position: fixed; -
griffinforms-form-builder/trunk/admin/html/formcontrols/form/entryalerts.php
r3299683 r3433300 34 34 protected function entryAlertsEmails() 35 35 { 36 $data = array(); 37 $data['max'] = 10; 38 $data['input_type'] = 'email'; 39 $data['placeholder'] = $this->lang->getText('admin_email_placeholder'); 40 $field = new \GriffinForms\Admin\Html\FormControls\MultiInput('entry_alerts_emails', $this->value, $data); 41 $field->html(); 36 $rows = $this->normalizeAlertEmailRows(); 37 $default_label = $this->lang->getText('default_message_label'); 38 39 echo '<div id="griffinforms-entryalertsemails-container" data-next-row-id="' . esc_attr($rows['next_row_id']) . '" data-max-emails="5">'; 40 foreach ($rows['rows'] as $row) { 41 $this->renderAlertEmailRow($row['row_id'], $row['email'], $row['message_id'], $row['message_name'], $row['message_heading'], $default_label); 42 } 43 echo '</div>'; 44 45 echo '<div class="row"><div class="col-md-7 text-center mt-3">'; 46 echo '<button type="button" id="griffinforms-entryalertsemails-add" class="gf-entryalert-add-btn btn btn-sm btn-dark">'; 47 $this->mIcon('add'); 48 echo '</button></div></div>'; 49 } 50 51 protected function normalizeAlertEmailRows(): array 52 { 53 $rows = []; 54 $row_id = 0; 55 56 if (is_array($this->value) && !empty($this->value)) { 57 $is_assoc = array_keys($this->value) !== range(0, count($this->value) - 1); 58 if ($is_assoc) { 59 foreach ($this->value as $email => $message_id) { 60 $row_id++; 61 $rows[] = $this->buildAlertEmailRow($row_id, $email, $message_id); 62 } 63 } else { 64 foreach ($this->value as $email) { 65 $row_id++; 66 $rows[] = $this->buildAlertEmailRow($row_id, $email, 0); 67 } 68 } 69 } 70 71 if (empty($rows)) { 72 $row_id++; 73 $rows[] = $this->buildAlertEmailRow($row_id, '', 0); 74 } 75 76 return [ 77 'rows' => $rows, 78 'next_row_id' => $row_id + 1, 79 ]; 80 } 81 82 protected function buildAlertEmailRow(int $row_id, $email, $message_id): array 83 { 84 $email = is_string($email) ? $email : ''; 85 $message_id = absint($message_id); 86 $message_name = ''; 87 $message_heading = ''; 88 89 if ($message_id > 0) { 90 $message = $this->sql->itemValues($message_id, 'message', ['name', 'heading']); 91 if ($message) { 92 $message_name = !empty($message->name) ? $message->name : 'Message ID ' . $message_id; 93 $message_heading = $message->heading ?? ''; 94 } 95 } 96 97 return [ 98 'row_id' => $row_id, 99 'email' => $email, 100 'message_id' => $message_id, 101 'message_name' => $message_name, 102 'message_heading' => $message_heading, 103 ]; 104 } 105 106 protected function truncateText(string $text, int $maxLength): string 107 { 108 if ($maxLength <= 0 || $text === '') { 109 return $text; 110 } 111 if (mb_strlen($text) <= $maxLength) { 112 return $text; 113 } 114 return mb_substr($text, 0, $maxLength - 3) . '...'; 115 } 116 117 protected function renderAlertEmailRow(int $row_id, string $email, int $message_id, string $message_name, string $message_heading, string $default_label): void 118 { 119 $select_label = $this->lang->getText('select_message_label'); 120 $change_label = $this->lang->getText('change_message_label'); 121 $remove_label = $this->lang->getText('remove_message_label'); 122 $placeholder = $this->lang->getText('admin_email_placeholder'); 123 124 $button_label = $message_id > 0 ? $change_label : $select_label; 125 $show_remove = $message_id > 0 ? '' : ' style="display: none;"'; 126 $message_name = $message_id > 0 ? $message_name : $default_label; 127 $message_heading = $message_id > 0 ? $message_heading : ''; 128 $message_name = $this->truncateText($message_name, 40); 129 $message_heading = $this->truncateText($message_heading, 48); 130 131 echo '<div class="gf-entryalert-row row align-items-center mb-2" data-alert-row-id="' . esc_attr($row_id) . '">'; 132 echo '<div class="col-md-3">'; 133 echo '<input type="email" class="form-control griffinforms-entryalertsemails-input" placeholder="' . esc_attr($placeholder) . '" value="' . esc_attr($email) . '">'; 134 echo '</div>'; 135 echo '<div class="col-md-3">'; 136 echo '<button type="button" class="button small btn-sm gf-entryalert-select-message me-2" data-alert-row-id="' . esc_attr($row_id) . '">' . esc_html($button_label) . '</button>'; 137 echo '<button type="button" class="button small btn-sm gf-entryalert-remove-message" data-alert-row-id="' . esc_attr($row_id) . '"' . $show_remove . '>' . esc_html($remove_label) . '</button>'; 138 echo '</div>'; 139 echo '<div class="col-md-4">'; 140 echo '<div class="gf-entryalert-message-preview">'; 141 echo '<span class="gf-entryalert-message-name">' . esc_html($message_name) . '</span>'; 142 if (!empty($message_heading)) { 143 echo '<br><span class="text-muted small gf-entryalert-message-heading">' . esc_html($message_heading) . '</span>'; 144 } else { 145 echo '<br><span class="text-muted small gf-entryalert-message-heading"></span>'; 146 } 147 echo '</div>'; 148 echo '</div>'; 149 echo '<div class="col-md-1 text-end">'; 150 echo '<button type="button" class="gf-entryalert-remove-row btn btn-sm btn-dark">'; 151 $this->mIcon('delete'); 152 echo '</button>'; 153 echo '</div>'; 154 echo '<input type="hidden" class="gf-entryalert-message-id" value="' . esc_attr($message_id) . '">'; 155 echo '</div>'; 156 } 157 158 public function fieldJs() 159 { 160 $select_label = $this->lang->getText('select_message_label'); 161 $change_label = $this->lang->getText('change_message_label'); 162 $remove_label = $this->lang->getText('remove_message_label'); 163 $default_label = $this->lang->getText('default_message_label'); 164 $placeholder = $this->lang->getText('admin_email_placeholder'); 165 166 $row_template = '<div class="gf-entryalert-row row align-items-center mb-2" data-alert-row-id="__ROW_ID__">' . 167 '<div class="col-md-3">' . 168 '<input type="email" class="form-control griffinforms-entryalertsemails-input" placeholder="' . esc_attr($placeholder) . '" value="">' . 169 '</div>' . 170 '<div class="col-md-3">' . 171 '<button type="button" class="button small btn-sm gf-entryalert-select-message me-2" data-alert-row-id="__ROW_ID__">' . esc_html($select_label) . '</button>' . 172 '<button type="button" class="button small btn-sm gf-entryalert-remove-message" data-alert-row-id="__ROW_ID__" style="display: none;">' . esc_html($remove_label) . '</button>' . 173 '</div>' . 174 '<div class="col-md-4">' . 175 '<div class="gf-entryalert-message-preview">' . 176 '<span class="gf-entryalert-message-name">' . esc_html($default_label) . '</span>' . 177 '<br><span class="text-muted small gf-entryalert-message-heading"></span>' . 178 '</div>' . 179 '</div>' . 180 '<div class="col-md-1 text-end">' . 181 '<button type="button" class="gf-entryalert-remove-row btn btn-sm btn-dark">' . wp_kses_post($this->mIcon('delete', 'rounded', 'return')) . '</button>' . 182 '</div>' . 183 '<input type="hidden" class="gf-entryalert-message-id" value="0">' . 184 '</div>'; 185 186 $js = ' 187 jQuery(document).ready(function($) { 188 var container = $("#griffinforms-entryalertsemails-container"); 189 var addBtn = $("#griffinforms-entryalertsemails-add"); 190 var rowTemplate = ' . json_encode($row_template) . '; 191 var selectLabel = ' . json_encode($select_label) . '; 192 var changeLabel = ' . json_encode($change_label) . '; 193 var defaultLabel = ' . json_encode($default_label) . '; 194 function truncateText(text, maxLength) { 195 if (!text) { return ""; } 196 return text.length > maxLength ? text.substring(0, maxLength - 3) + "..." : text; 197 } 198 var maxEmails = parseInt(container.attr("data-max-emails"), 10) || 5; 199 200 var selectModal = $("#griffinforms-selectmessage-modal"); 201 if (selectModal.length) { 202 selectModal.appendTo("body"); 203 } 204 var createModal = $("#griffinforms-createmessage-modal"); 205 if (createModal.length) { 206 createModal.appendTo("body"); 207 } 208 209 function nextRowId() { 210 var current = parseInt(container.attr("data-next-row-id"), 10) || 1; 211 container.attr("data-next-row-id", current + 1); 212 return current; 213 } 214 215 function ensureAtLeastOneRow() { 216 if (container.find(".gf-entryalert-row").length === 0) { 217 addRow(); 218 } 219 } 220 221 function updateRemoveRowVisibility() { 222 var rows = container.find(".gf-entryalert-row"); 223 if (rows.length <= 1) { 224 rows.find(".gf-entryalert-remove-row").hide(); 225 } else { 226 rows.find(".gf-entryalert-remove-row").show(); 227 } 228 229 if (rows.length >= maxEmails) { 230 addBtn.hide(); 231 } else { 232 addBtn.show(); 233 } 234 } 235 236 function addRow() { 237 var rowId = nextRowId(); 238 var html = rowTemplate.replace(/__ROW_ID__/g, rowId); 239 container.append(html); 240 updateRemoveRowVisibility(); 241 } 242 243 function resetMessageSelection(row) { 244 row.find(".gf-entryalert-message-id").val("0"); 245 row.find(".gf-entryalert-message-name").text(defaultLabel); 246 row.find(".gf-entryalert-message-heading").text(""); 247 row.find(".gf-entryalert-select-message").text(selectLabel); 248 row.find(".gf-entryalert-remove-message").hide(); 249 } 250 251 addBtn.on("click", function() { 252 if (container.find(".gf-entryalert-row").length >= maxEmails) { 253 return; 254 } 255 addRow(); 256 }); 257 258 container.on("click", ".gf-entryalert-remove-row", function() { 259 $(this).closest(".gf-entryalert-row").remove(); 260 ensureAtLeastOneRow(); 261 updateRemoveRowVisibility(); 262 }); 263 264 container.on("click", ".gf-entryalert-remove-message", function() { 265 var row = $(this).closest(".gf-entryalert-row"); 266 resetMessageSelection(row); 267 }); 268 269 container.on("click", ".gf-entryalert-select-message", function() { 270 var row = $(this).closest(".gf-entryalert-row"); 271 var rowId = row.attr("data-alert-row-id"); 272 var modal = $("#griffinforms-selectmessage-modal"); 273 modal.data("gf-trigger", "map_alert_email_message"); 274 modal.data("gf-alert-row-id", rowId); 275 modal.modal("show"); 276 }); 277 278 window.griffinformsSaveAlertEmailMapping = function(rowId, itemId, itemName, itemHeading) { 279 var row = container.find(".gf-entryalert-row[data-alert-row-id=\'" + rowId + "\']"); 280 if (!row.length) { return; } 281 row.find(".gf-entryalert-message-id").val(itemId); 282 row.find(".gf-entryalert-message-name").text(truncateText(itemName || defaultLabel, 30)); 283 row.find(".gf-entryalert-message-heading").text(truncateText(itemHeading || "", 38)); 284 row.find(".gf-entryalert-select-message").text(changeLabel); 285 row.find(".gf-entryalert-remove-message").show(); 286 }; 287 288 updateRemoveRowVisibility(); 289 });'; 290 291 wp_add_inline_script('griffinforms-global-js', $js); 42 292 } 43 293 } -
griffinforms-form-builder/trunk/admin/html/modals/selectitem.php
r3310004 r3433300 172 172 if ( 173 173 $(caller).attr("id") === "<?php echo esc_attr($this->ids['open_btn']); ?>" || 174 $(caller).data("gf-trigger") === "map_email_message" 174 $(caller).data("gf-trigger") === "map_email_message" || 175 $(caller).data("gf-trigger") === "map_alert_email_message" 175 176 ) { 176 177 getItemsForModal(0, "list_empty", caller); … … 287 288 function handleEmailFieldMessageMapping(itemId, itemName, itemHeading) { 288 289 var selectionModal = $("#<?php echo esc_attr($this->id); ?>"); 290 if (selectionModal.data("gf-trigger") !== "map_email_message") { 291 return; 292 } 289 293 var fieldId = selectionModal.data("gf-email-field-id"); 290 294 var parentModalId = selectionModal.data("gf-parentmodalid"); … … 300 304 } 301 305 } 306 } 307 308 // Handle mapping for admin alert email rows (no parent modal). 309 function handleAlertEmailMessageMapping(itemId, itemName, itemHeading) { 310 var selectionModal = $("#<?php echo esc_attr($this->id); ?>"); 311 if (selectionModal.data("gf-trigger") !== "map_alert_email_message") { 312 return; 313 } 314 var rowId = selectionModal.data("gf-alert-row-id"); 315 if (!rowId) { 316 return; 317 } 318 319 if (typeof window.griffinformsSaveAlertEmailMapping === "function") { 320 window.griffinformsSaveAlertEmailMapping(rowId, itemId, itemName, itemHeading); 321 } 322 323 selectionModal.modal("hide"); 302 324 } 303 325 … … 311 333 312 334 handleEmailFieldMessageMapping(itemId, itemName, itemHeading); 335 handleAlertEmailMessageMapping(itemId, itemName, itemHeading); 313 336 314 337 itemIcon = $(this).find(".griffinforms-modalitemlist-icon").attr("src"); -
griffinforms-form-builder/trunk/admin/html/pages/single/format.php
r3299683 r3433300 110 110 public function getOption($prop, $wide = false) 111 111 { 112 $label_class = 'col-md-3 ';112 $label_class = 'col-md-3 ps-3'; 113 113 $input_class = 'col-md-9'; 114 114 115 115 if ($wide) { 116 $label_class = 'col-md-12 ';117 $input_class = 'col-md-12 ';116 $label_class = 'col-md-12 ps-3 mb-3'; 117 $input_class = 'col-md-12 ps-3'; 118 118 } 119 119 … … 153 153 } 154 154 155 echo '<label class="fw-bold ps-3">' . wp_kses_post($this->lang->getText($this->prop, 'label') . ' ' . $label_suffix) . '</label>';155 echo '<label class="fw-bold">' . wp_kses_post($this->lang->getText($this->prop, 'label') . ' ' . $label_suffix) . '</label>'; 156 156 } 157 157 -
griffinforms-form-builder/trunk/admin/html/pages/single/message.php
r3299683 r3433300 15 15 { 16 16 echo '<div class="container border-bottom griffinforms-item-options w-75 mt-3 pt-3">'; 17 $this->getOption('content'); 17 $this->mergeTokenTools(); 18 $this->getOption('content', true); 18 19 echo '</div>'; 19 20 } … … 36 37 $this->richText(); 37 38 } 39 40 protected function mergeTokenTools() 41 { 42 $forms = $this->sql->getFormContextOptions(); 43 44 echo '<div class="gf-merge-token-panel mb-3">'; 45 echo '<div class="row mb-2">'; 46 echo '<div class="col-md-12 small text-muted">' . esc_html($this->lang->getText('merge_tokens_description')) . '</div>'; 47 echo '</div>'; 48 49 echo '<div class="row g-2 align-items-end">'; 50 echo '<div class="col-md-3">'; 51 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_form_context_label')) . '</label>'; 52 echo '<select class="form-select form-select-sm" id="griffinforms-merge-form-context">'; 53 echo '<option value="">' . esc_html($this->lang->getText('select_form')) . '</option>'; 54 foreach ($forms as $form) { 55 $label = !empty($form->name) ? $form->name : ('Form ' . $form->id); 56 if (!empty($form->heading) && $form->heading !== $label) { 57 $label .= ' - ' . $form->heading; 58 } 59 echo '<option value="' . esc_attr($form->id) . '">' . esc_html($label) . '</option>'; 60 } 61 echo '</select>'; 62 echo '</div>'; 63 64 echo '<div class="col-md-2">'; 65 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_item_type_label')) . '</label>'; 66 echo '<select class="form-select form-select-sm" id="griffinforms-merge-item-type">'; 67 echo '<option value="field">' . esc_html($this->lang->getText('merge_item_type_field')) . '</option>'; 68 echo '<option value="form">' . esc_html($this->lang->getText('merge_item_type_form')) . '</option>'; 69 echo '<option value="submission">' . esc_html($this->lang->getText('merge_item_type_submission')) . '</option>'; 70 echo '</select>'; 71 echo '</div>'; 72 73 echo '<div class="col-md-3">'; 74 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_field_label')) . '</label>'; 75 echo '<div class="d-flex align-items-center gap-2">'; 76 echo '<select class="form-select form-select-sm" id="griffinforms-merge-field-select">'; 77 echo '<option value="">' . esc_html($this->lang->getText('select_field')) . '</option>'; 78 echo '</select>'; 79 echo '<span class="spinner is-active" id="griffinforms-merge-field-spinner" style="visibility: hidden;"></span>'; 80 echo '</div>'; 81 echo '</div>'; 82 83 echo '<div class="col-md-2">'; 84 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_attr_label')) . '</label>'; 85 echo '<select class="form-select form-select-sm" id="griffinforms-merge-attr-select"></select>'; 86 echo '</div>'; 87 88 echo '<div class="col-md-2">'; 89 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_fallback_label')) . '</label>'; 90 echo '<input type="text" class="form-control form-control-sm" id="griffinforms-merge-fallback">'; 91 echo '</div>'; 92 echo '</div>'; 93 94 echo '<div class="row g-2 align-items-end mt-2">'; 95 echo '<div class="col-md-10">'; 96 echo '<label class="form-label small">' . esc_html($this->lang->getText('merge_token_preview_label')) . '</label>'; 97 echo '<input type="text" class="form-control form-control-sm" id="griffinforms-merge-token-preview" readonly>'; 98 echo '</div>'; 99 100 echo '<div class="col-md-2">'; 101 echo '<div class="d-flex justify-content-end align-items-center gap-2">'; 102 echo '<span class="spinner is-active" id="griffinforms-merge-token-spinner" style="visibility: hidden;"></span>'; 103 echo '<button type="button" class="button button-primary btn-sm" id="griffinforms-merge-token-insert">' . esc_html($this->lang->getText('merge_insert_token_label')) . '</button>'; 104 echo '</div>'; 105 echo '</div>'; 106 echo '</div>'; 107 echo '</div>'; 108 } 38 109 } -
griffinforms-form-builder/trunk/admin/js/local/form.php
r3299683 r3433300 124 124 var sendTo, fieldValue; 125 125 126 fieldValue = [];126 fieldValue = {}; 127 127 128 128 sendTo = $("input[name=entry_alerts_to]:checked").val(); 129 129 130 130 if (sendTo === "specific_emails") { 131 132 $(".griffinforms-entryalertsemails-input").each(function (index) { 133 $(this).removeClass("border-danger"); 134 135 if (isValidEmail($(this).val()) == false) { 131 var validCount = 0; 132 var maxEmails = 5; 133 134 $(".gf-entryalert-row").each(function () { 135 var emailInput = $(this).find(".griffinforms-entryalertsemails-input"); 136 var messageIdInput = $(this).find(".gf-entryalert-message-id"); 137 var emailValue = $.trim(emailInput.val()); 138 var messageId = parseInt(messageIdInput.val(), 10); 139 140 emailInput.removeClass("border-danger"); 141 142 if (!emailValue) { 143 return; 144 } 145 146 if (isValidEmail(emailValue) == false) { 136 147 showItemError("' . esc_js($this->lang->getText('invalid_email')) . '", "griffinforms-entryalertsemails-container"); 137 $(this).addClass("border-danger"); 138 } else { 139 fieldValue.push($(this).val()); 148 emailInput.addClass("border-danger"); 149 return; 150 } 151 152 if (!messageId || isNaN(messageId)) { 153 messageId = 0; 154 } 155 156 if (validCount < maxEmails) { 157 fieldValue[emailValue] = messageId; 158 validCount++; 140 159 } 141 160 }); 161 162 if (validCount === 0) { 163 showItemError("' . esc_js($this->lang->getText('invalid_email')) . '", "griffinforms-entryalertsemails-container"); 164 $(".griffinforms-entryalertsemails-input").each(function () { 165 if (!$.trim($(this).val())) { 166 $(this).addClass("border-danger"); 167 } 168 }); 169 } 142 170 } 143 171 -
griffinforms-form-builder/trunk/admin/js/local/message.php
r3299683 r3433300 5 5 class Message extends \GriffinForms\Admin\Js\Local\Single 6 6 { 7 protected function localJs() 8 { 9 $js = parent::localJs(); 10 $js .= $this->mergeTokenBuilderJs() . PHP_EOL; 11 return $js; 12 } 13 7 14 protected function getContent() 8 15 { … … 13 20 }'; 14 21 } 22 23 protected function mergeTokenBuilderJs() 24 { 25 $nonce = wp_create_nonce('get_form_fields'); 26 27 $js = ' 28 function griffinformsMergeTokenInit() { 29 var formSelect = $("#griffinforms-merge-form-context"); 30 var itemTypeSelect = $("#griffinforms-merge-item-type"); 31 var fieldSelect = $("#griffinforms-merge-field-select"); 32 var attrSelect = $("#griffinforms-merge-attr-select"); 33 var fallbackInput = $("#griffinforms-merge-fallback"); 34 var previewInput = $("#griffinforms-merge-token-preview"); 35 var insertBtn = $("#griffinforms-merge-token-insert"); 36 var fieldSpinner = $("#griffinforms-merge-field-spinner"); 37 var tokenSpinner = $("#griffinforms-merge-token-spinner"); 38 39 if (!formSelect.length) { 40 return; 41 } 42 43 var formAttrs = [ 44 { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" }, 45 { value: "name", label: "' . esc_js($this->lang->getText('merge_attr_name_label')) . '" }, 46 { value: "heading", label: "' . esc_js($this->lang->getText('merge_attr_heading_label')) . '" }, 47 { value: "description", label: "' . esc_js($this->lang->getText('merge_attr_description_label')) . '" } 48 ]; 49 50 var fieldAttrs = [ 51 { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" }, 52 { value: "name", label: "' . esc_js($this->lang->getText('merge_attr_name_label')) . '" }, 53 { value: "label", label: "' . esc_js($this->lang->getText('merge_attr_label_label')) . '" }, 54 { value: "value", label: "' . esc_js($this->lang->getText('merge_attr_value_label')) . '" }, 55 { value: "answer_count", label: "' . esc_js($this->lang->getText('merge_attr_answer_count_label')) . '" }, 56 { value: "description", label: "' . esc_js($this->lang->getText('merge_attr_helptext_label')) . '" } 57 ]; 58 59 var submissionAttrs = [ 60 { value: "id", label: "' . esc_js($this->lang->getText('merge_attr_id_label')) . '" }, 61 { value: "position", label: "' . esc_js($this->lang->getText('merge_attr_position_label')) . '" }, 62 { value: "time", label: "' . esc_js($this->lang->getText('merge_attr_time_label')) . '" }, 63 { value: "data", label: "' . esc_js($this->lang->getText('merge_attr_data_label')) . '" }, 64 { value: "attachments_count", label: "' . esc_js($this->lang->getText('merge_attr_attachments_count_label')) . '" }, 65 { value: "attachments_size", label: "' . esc_js($this->lang->getText('merge_attr_attachments_size_label')) . '" }, 66 { value: "payment_total", label: "' . esc_js($this->lang->getText('merge_attr_payment_total_label')) . '" }, 67 { value: "payment_breakdown", label: "' . esc_js($this->lang->getText('merge_attr_payment_breakdown_label')) . '" }, 68 { value: "payment_status", label: "' . esc_js($this->lang->getText('merge_attr_payment_status_label')) . '" } 69 ]; 70 71 function populateAttrs(list, preferredValue) { 72 attrSelect.empty(); 73 list.forEach(function(item) { 74 var option = $("<option>").val(item.value).text(item.label); 75 if (preferredValue && item.value === preferredValue) { 76 option.prop("selected", true); 77 } 78 attrSelect.append(option); 79 }); 80 } 81 82 function loadFields(formId) { 83 fieldSelect.empty(); 84 fieldSelect.append($("<option>").val("").text("' . esc_js($this->lang->getText('select_field')) . '")); 85 if (!formId) { 86 return; 87 } 88 89 fieldSpinner.css("visibility", "visible"); 90 tokenSpinner.css("visibility", "visible"); 91 insertBtn.prop("disabled", true); 92 93 $.post(ajaxurl, { 94 action: "griffinforms_get_form_fields", 95 form_id: formId, 96 nonce: "' . esc_js($nonce) . '" 97 }, function(response) { 98 if (typeof response !== "object") { 99 try { 100 response = JSON.parse(response); 101 } catch (e) { 102 return; 103 } 104 } 105 var fields = response.fields || (response.data && response.data.fields) || []; 106 if (!response || !response.success || !Array.isArray(fields)) { 107 return; 108 } 109 fields.forEach(function(field) { 110 var label = field.heading || field.name || ("Field " + field.id); 111 fieldSelect.append($("<option>").val(field.id).text(label)); 112 }); 113 }).always(function() { 114 fieldSpinner.css("visibility", "hidden"); 115 tokenSpinner.css("visibility", "hidden"); 116 updatePreview(); 117 }); 118 } 119 120 function toggleFieldSelect() { 121 if (itemTypeSelect.val() === "form" || itemTypeSelect.val() === "submission") { 122 fieldSelect.prop("disabled", true); 123 } else { 124 fieldSelect.prop("disabled", false); 125 } 126 } 127 128 function toggleAttrSelect() { 129 if (itemTypeSelect.val() === "form" || itemTypeSelect.val() === "submission") { 130 attrSelect.prop("disabled", false); 131 return; 132 } 133 var fieldId = parseInt(fieldSelect.val(), 10); 134 attrSelect.prop("disabled", !(fieldId > 0)); 135 } 136 137 function buildToken() { 138 var itemType = itemTypeSelect.val(); 139 var attr = attrSelect.val(); 140 var fallback = $.trim(fallbackInput.val()); 141 var token = { 142 item_type: itemType, 143 attr: attr 144 }; 145 146 if (itemType === "field") { 147 var fieldId = parseInt(fieldSelect.val(), 10); 148 if (fieldId > 0) { 149 token.item_id = fieldId; 150 } 151 } 152 153 if (fallback !== "") { 154 token.fallback = fallback; 155 } 156 157 return "{{json:" + JSON.stringify(token) + "}}"; 158 } 159 160 function updatePreview() { 161 previewInput.val(buildToken()); 162 toggleInsert(); 163 toggleAttrSelect(); 164 } 165 166 function toggleInsert() { 167 var itemType = itemTypeSelect.val(); 168 if (itemType === "form" || itemType === "submission") { 169 insertBtn.prop("disabled", !attrSelect.val()); 170 return; 171 } 172 var fieldId = parseInt(fieldSelect.val(), 10); 173 insertBtn.prop("disabled", !(fieldId > 0 && attrSelect.val())); 174 } 175 176 function insertToken() { 177 var token = buildToken(); 178 var editorId = "griffinforms-content-richtext"; 179 if (window.wp && wp.editor && typeof wp.editor.insertContent === "function") { 180 wp.editor.insertContent(token); 181 return; 182 } 183 if (window.tinymce && tinymce.get(editorId)) { 184 tinymce.get(editorId).execCommand("mceInsertContent", false, token); 185 return; 186 } 187 var textarea = $("#" + editorId); 188 if (textarea.length) { 189 var current = textarea.val(); 190 textarea.val(current + token); 191 } 192 } 193 194 formSelect.on("change", function() { 195 loadFields($(this).val()); 196 updatePreview(); 197 }); 198 199 itemTypeSelect.on("change", function() { 200 if ($(this).val() === "form") { 201 populateAttrs(formAttrs, "name"); 202 } else if ($(this).val() === "submission") { 203 populateAttrs(submissionAttrs, "id"); 204 } else { 205 populateAttrs(fieldAttrs, "value"); 206 } 207 toggleFieldSelect(); 208 updatePreview(); 209 }); 210 211 fieldSelect.on("change", updatePreview); 212 attrSelect.on("change", updatePreview); 213 fallbackInput.on("input", updatePreview); 214 insertBtn.on("click", insertToken); 215 216 populateAttrs(fieldAttrs, "value"); 217 toggleFieldSelect(); 218 updatePreview(); 219 } 220 221 griffinformsMergeTokenInit(); 222 '; 223 224 return $js; 225 } 15 226 } -
griffinforms-form-builder/trunk/admin/language/form.php
r3299683 r3433300 408 408 } 409 409 410 protected function defaultMessageLabel() 411 { 412 return __('Default message', 'griffinforms-form-builder'); 413 } 414 410 415 protected function selectMessage() 411 416 { -
griffinforms-form-builder/trunk/admin/language/message.php
r3299683 r3433300 52 52 { 53 53 return __('Write the content of your message. This content will be used in emails, so make sure it is clear and concise. Always review the message for accuracy before saving.', 'griffinforms-form-builder'); 54 } 55 56 protected function mergeTokensDescription() 57 { 58 return __('Build a JSON merge token for this message. Tokens are resolved at send time using the submission context. Tip: Submission attributes are form-agnostic, while Field attributes require a selected form for lookup.', 'griffinforms-form-builder'); 59 } 60 61 protected function mergeFormContextLabel() 62 { 63 return __('Form Context (Preview)', 'griffinforms-form-builder'); 64 } 65 66 protected function mergeItemTypeLabel() 67 { 68 return __('Item Type', 'griffinforms-form-builder'); 69 } 70 71 protected function mergeItemTypeField() 72 { 73 return __('Field', 'griffinforms-form-builder'); 74 } 75 76 protected function mergeItemTypeForm() 77 { 78 return __('Form', 'griffinforms-form-builder'); 79 } 80 81 protected function mergeItemTypeSubmission() 82 { 83 return __('Submission', 'griffinforms-form-builder'); 84 } 85 86 protected function mergeFieldLabel() 87 { 88 return __('Field', 'griffinforms-form-builder'); 89 } 90 91 protected function mergeAttrLabel() 92 { 93 return __('Attribute', 'griffinforms-form-builder'); 94 } 95 96 protected function mergeFallbackLabel() 97 { 98 return __('Fallback', 'griffinforms-form-builder'); 99 } 100 101 102 protected function mergeTokenPreviewLabel() 103 { 104 return __('Token Preview', 'griffinforms-form-builder'); 105 } 106 107 protected function mergeInsertTokenLabel() 108 { 109 return __('Insert Token', 'griffinforms-form-builder'); 110 } 111 112 protected function selectForm() 113 { 114 return __('Select Form', 'griffinforms-form-builder'); 115 } 116 117 protected function selectField() 118 { 119 return __('Select Field', 'griffinforms-form-builder'); 120 } 121 122 protected function mergeAttrIdLabel() 123 { 124 return __('ID', 'griffinforms-form-builder'); 125 } 126 127 protected function mergeAttrNameLabel() 128 { 129 return __('Name', 'griffinforms-form-builder'); 130 } 131 132 protected function mergeAttrHeadingLabel() 133 { 134 return __('Heading', 'griffinforms-form-builder'); 135 } 136 137 protected function mergeAttrDescriptionLabel() 138 { 139 return __('Description', 'griffinforms-form-builder'); 140 } 141 142 protected function mergeAttrHelptextLabel() 143 { 144 return __('Helptext', 'griffinforms-form-builder'); 145 } 146 147 protected function mergeAttrLabelLabel() 148 { 149 return __('Label', 'griffinforms-form-builder'); 150 } 151 152 protected function mergeAttrValueLabel() 153 { 154 return __('Value', 'griffinforms-form-builder'); 155 } 156 157 protected function mergeAttrAnswerCountLabel() 158 { 159 return __('Number of Answers', 'griffinforms-form-builder'); 160 } 161 162 protected function mergeAttrPositionLabel() 163 { 164 return __('Position', 'griffinforms-form-builder'); 165 } 166 167 protected function mergeAttrTimeLabel() 168 { 169 return __('Time', 'griffinforms-form-builder'); 170 } 171 172 protected function mergeAttrDataLabel() 173 { 174 return __('Submission Data', 'griffinforms-form-builder'); 175 } 176 177 protected function mergeAttrAttachmentsCountLabel() 178 { 179 return __('Attachments Count', 'griffinforms-form-builder'); 180 } 181 182 protected function mergeAttrAttachmentsSizeLabel() 183 { 184 return __('Attachments Size', 'griffinforms-form-builder'); 185 } 186 187 protected function mergeAttrPaymentTotalLabel() 188 { 189 return __('Payment Total', 'griffinforms-form-builder'); 190 } 191 192 protected function mergeAttrPaymentBreakdownLabel() 193 { 194 return __('Payment Breakdown', 'griffinforms-form-builder'); 195 } 196 197 protected function mergeAttrPaymentStatusLabel() 198 { 199 return __('Payment Status', 'griffinforms-form-builder'); 54 200 } 55 201 -
griffinforms-form-builder/trunk/admin/secure/form.php
r3299683 r3433300 132 132 protected function secureAlertEmails($emails) 133 133 { 134 $secured_emails_array = array_map('sanitize_email', $emails); 135 return $secured_emails_array; 134 if (!is_array($emails)) { 135 return []; 136 } 137 138 $secured = []; 139 $keys = array_keys($emails); 140 $is_assoc = $keys !== range(0, count($emails) - 1); 141 142 if ($is_assoc) { 143 foreach ($emails as $email => $message_id) { 144 $clean_email = sanitize_email($email); 145 if (empty($clean_email)) { 146 continue; 147 } 148 $secured[$clean_email] = absint($message_id); 149 } 150 return $secured; 151 } 152 153 foreach ($emails as $email) { 154 $clean_email = sanitize_email($email); 155 if (!empty($clean_email)) { 156 $secured[$clean_email] = 0; 157 } 158 } 159 160 return $secured; 136 161 } 137 162 -
griffinforms-form-builder/trunk/admin/sql/message.php
r3299683 r3433300 5 5 class Message extends \GriffinForms\Admin\Sql\Format 6 6 { 7 public function getFormContextOptions(): array 8 { 9 global $wpdb; 10 11 $forms_table = $this->config->getTable('form'); 12 if (empty($forms_table)) { 13 return []; 14 } 15 16 $rows = $wpdb->get_results( 17 $wpdb->prepare( 18 'SELECT id, name, heading FROM %i ORDER BY id DESC', 19 $forms_table 20 ) 21 ); 22 23 return is_array($rows) ? $rows : []; 24 } 7 25 } -
griffinforms-form-builder/trunk/config.php
r3425584 r3433300 5 5 class Config 6 6 { 7 public const VERSION = '2.1. 2.0';7 public const VERSION = '2.1.3.0'; 8 8 public const DB_VER = '1.0'; 9 9 public const PHP_REQUIRED = '8.2'; -
griffinforms-form-builder/trunk/frontend/actions/postsubmission.php
r3425584 r3433300 24 24 25 25 /** 26 * Normalize alert email data into a map of email => message_id. 27 * 28 * @param array $alert_emails 29 * @return array<string,int> 30 */ 31 protected function normalizeAlertEmailMap(array $alert_emails): array 32 { 33 if (empty($alert_emails)) { 34 return []; 35 } 36 37 $keys = array_keys($alert_emails); 38 $is_assoc = $keys !== range(0, count($alert_emails) - 1); 39 $normalized = []; 40 $form_id = $this->getFormId(); 41 42 if ($is_assoc) { 43 foreach ($alert_emails as $email => $message_id) { 44 $clean_email = sanitize_email($email); 45 if (empty($clean_email)) { 46 if ($form_id > 0) { 47 $this->log->add( 48 'warning', 49 'form', 50 $form_id, 51 get_current_user_id(), 52 'Admin alert email ignored: invalid recipient address in mapping.', 53 $this->getMailLogCategory() 54 ); 55 } 56 continue; 57 } 58 $normalized[$clean_email] = absint($message_id); 59 } 60 return $normalized; 61 } 62 63 if ($form_id > 0) { 64 $this->log->add( 65 'info', 66 'form', 67 $form_id, 68 get_current_user_id(), 69 'Legacy alert_emails format detected; normalized to message mapping with default template.', 70 $this->getMailLogCategory() 71 ); 72 } 73 74 foreach ($alert_emails as $email) { 75 $clean_email = sanitize_email($email); 76 if (!empty($clean_email)) { 77 $normalized[$clean_email] = 0; 78 } elseif ($form_id > 0) { 79 $this->log->add( 80 'warning', 81 'form', 82 $form_id, 83 get_current_user_id(), 84 'Admin alert email ignored: invalid recipient address in legacy list.', 85 $this->getMailLogCategory() 86 ); 87 } 88 } 89 90 return $normalized; 91 } 92 93 /** 26 94 * Processes and sends admin notification emails based on configured alert emails. 27 95 */ … … 29 97 { 30 98 $this->data['form'] = $this->form; 31 $email_alert = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($this->data);32 99 $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0; 100 $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine(); 101 $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id); 102 $merge_context['form_object'] = $this->form; 103 $form_id = $this->getFormId(); 104 if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) { 105 $this->log->add( 106 'warning', 107 'submission', 108 $submission_id, 109 get_current_user_id(), 110 'Merge context missing for admin alert; tokens may not resolve.', 111 $this->getMailLogCategory() 112 ); 113 } 33 114 $mailer = new \GriffinForms\Includes\Mailer\MailHandler(); 34 115 $mailer->setLogContext('submission', $submission_id); 35 116 $mailer->setSubmissionId($submission_id); 36 117 37 $mailer->setEmailSubject($email_alert->getSubject()); 38 $mailer->setEmailContent($email_alert->getContent()); 39 40 foreach ($this->alert_emails as $email) { 41 $mailer->setEmailRecipient($email); 118 $alert_map = $this->normalizeAlertEmailMap($this->alert_emails); 119 $default_template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($this->data); 120 $default_subject = $merge_engine->render($default_template->getSubject(), $merge_context); 121 $default_content = $merge_engine->render($default_template->getContent(), $merge_context); 122 123 foreach ($alert_map as $email => $message_id) { 124 $subject = ''; 125 $content = ''; 126 $has_custom_message = false; 127 if ($message_id > 0) { 128 $message = $this->sql->getItemById($message_id, 'message'); 129 if (is_array($message)) { 130 $subject = $merge_engine->render($message['name'] ?? '', $merge_context); 131 $content = $merge_engine->render($message['content'] ?? '', $merge_context); 132 $has_custom_message = true; 133 if ($submission_id > 0 && trim($content) === '') { 134 $this->log->add( 135 'warning', 136 'submission', 137 $submission_id, 138 get_current_user_id(), 139 sprintf('Admin alert message %d has empty content; using default template.', $message_id), 140 $this->getMailLogCategory() 141 ); 142 } 143 } elseif ($submission_id > 0) { 144 $this->log->add( 145 'warning', 146 'submission', 147 $submission_id, 148 get_current_user_id(), 149 sprintf('Admin alert message %d no longer exists; using default template.', $message_id), 150 $this->getMailLogCategory() 151 ); 152 } 153 } 154 155 if ($subject === '' && $content === '') { 156 $subject = $default_subject; 157 $content = $default_content; 158 } elseif (trim($content) === '') { 159 $content = $default_content; 160 if ($subject === '') { 161 $subject = $default_subject; 162 } 163 } elseif ($has_custom_message && $subject === '') { 164 $subject = $default_subject; 165 } 166 167 $mailer->setEmailSubject($subject); 168 $mailer->setEmailContent($content); 169 $mailer->setEmailRecipient($email); 42 170 $mailer->sendMail(); 43 171 } … … 127 255 public function processUserAutoresponderMappings($mappings) 128 256 { 257 $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0; 258 $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine(); 259 $merge_context = \GriffinForms\Includes\Pipelines\SubmissionContextBuilder::getInstance()->build($submission_id); 260 $merge_context['form_object'] = $this->form; 261 $form_id = $this->getFormId(); 262 if ($submission_id > 0 && (empty($merge_context) || empty($merge_context['submission']))) { 263 $this->log->add( 264 'warning', 265 'submission', 266 $submission_id, 267 get_current_user_id(), 268 'Merge context missing for autoresponder; tokens may not resolve.', 269 $this->getMailLogCategory() 270 ); 271 } 272 129 273 foreach ($mappings as $mapping) { 130 274 $email_field_id = $mapping['email_field_id']; … … 160 304 } 161 305 if (!is_email($email_value)) { 306 if ($submission_id > 0) { 307 $this->log->add( 308 'warning', 309 'submission', 310 $submission_id, 311 get_current_user_id(), 312 sprintf( 313 'Autoresponder email ignored: mapped field ID %d contains an invalid email.', 314 $email_field_id 315 ), 316 $this->getMailLogCategory() 317 ); 318 } 162 319 $this->log->add( 163 320 'error', … … 177 334 178 335 if (!$message) { 336 if ($submission_id > 0) { 337 $this->log->add( 338 'warning', 339 'submission', 340 $submission_id, 341 get_current_user_id(), 342 sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id), 343 $this->getMailLogCategory() 344 ); 345 } 179 346 $this->log->add( 180 347 'error', … … 188 355 } 189 356 190 $submission_id = isset($this->data['submission']['id']) ? (int) $this->data['submission']['id'] : 0;191 357 $mailer = new \GriffinForms\Includes\Mailer\MailHandler(); 192 358 $mailer->setLogContext('submission', $submission_id); 193 359 $mailer->setSubmissionId($submission_id); 194 $mailer->setEmailSubject($message['name'] ?? ''); 195 $mailer->setEmailContent($message['content'] ?? ''); 360 $subject = $merge_engine->render($message['name'] ?? '', $merge_context); 361 $content = $merge_engine->render($message['content'] ?? '', $merge_context); 362 if ($submission_id > 0 && trim($content) === '') { 363 $this->log->add( 364 'warning', 365 'submission', 366 $submission_id, 367 get_current_user_id(), 368 sprintf('Autoresponder message %d has empty content; email not sent.', $message_id), 369 $this->getMailLogCategory() 370 ); 371 continue; 372 } 373 $mailer->setEmailSubject($subject); 374 $mailer->setEmailContent($content); 196 375 $mailer->setEmailRecipient($email_value); 197 376 … … 513 692 514 693 $email_alert = $this->isEmailAlert(); 515 $alert_emails = $email_alert ? array_map('sanitize_email',$this->alert_emails) : [];694 $alert_emails = $email_alert ? $this->normalizeAlertEmailMap($this->alert_emails) : []; 516 695 517 696 $has_autoresponder = $this->hasUserAutoresponderMappings(); … … 598 777 return $this->mail_log_category; 599 778 } 779 780 private function getFormId(): int 781 { 782 if ($this->form && method_exists($this->form, 'getProp')) { 783 $form_id = (int) ($this->form->getProp('id') ?? 0); 784 if ($form_id > 0) { 785 return $form_id; 786 } 787 } 788 789 return isset($this->data['form']) ? (int) $this->data['form'] : 0; 790 } 600 791 } -
griffinforms-form-builder/trunk/griffinforms.php
r3425584 r3433300 4 4 * Plugin URI: https://griffinforms.com/ 5 5 * Description: A powerful and flexible form builder for WordPress. Create multi-page forms with drag-and-drop ease, custom validations, and full submission management. 6 * Version: 2.1. 2.06 * Version: 2.1.3.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 8.2 -
griffinforms-form-builder/trunk/includes/pipelines/handlers/mailqueuehandler.php
r3421663 r3433300 74 74 private function queueAdminAlerts(array $recipients, int $submission_id, int $form_id): void 75 75 { 76 if (empty($recipients)) { 77 return; 78 } 79 80 $keys = array_keys($recipients); 81 $is_assoc = $keys !== range(0, count($recipients) - 1); 82 83 if ($is_assoc) { 84 foreach ($recipients as $email => $message_id) { 85 $email = sanitize_email($email); 86 if (empty($email)) { 87 continue; 88 } 89 $this->repository->enqueue( 90 'mail_admin_alert', 91 'mailer', 92 $submission_id, 93 $form_id, 94 [ 95 'recipient' => $email, 96 'message_id' => (int) $message_id, 97 ], 98 10 99 ); 100 } 101 return; 102 } 103 76 104 foreach ($recipients as $recipient) { 77 105 $email = sanitize_email($recipient); -
griffinforms-form-builder/trunk/includes/pipelines/jobworker.php
r3424123 r3433300 15 15 /** @var \GriffinForms\Log */ 16 16 protected $log; 17 18 /** @var string|null */ 19 protected $mail_log_category; 17 20 18 21 public function __construct() … … 67 70 { 68 71 $recipient = sanitize_email($payload['recipient'] ?? ''); 72 $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0; 73 $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0; 74 $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0; 69 75 if (empty($recipient)) { 70 throw new \RuntimeException('Recipient missing for admin alert'); 76 $this->log->add( 77 'warning', 78 'submission', 79 $submission_id, 80 0, 81 'Admin alert email ignored: invalid recipient address.', 82 $this->getMailLogCategory() 83 ); 84 return; 71 85 } 72 86 … … 74 88 $form = $this->loadForm((int) ($job['form_id'] ?? 0)); 75 89 $context['form_object'] = $form; 90 if ($submission_id > 0 && (empty($context) || empty($context['submission']))) { 91 $this->log->add( 92 'warning', 93 'submission', 94 $submission_id, 95 0, 96 'Merge context missing for admin alert; tokens may not resolve.', 97 $this->getMailLogCategory() 98 ); 99 } 76 100 77 101 $template_data = [ … … 80 104 ]; 81 105 82 $template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($template_data); 106 $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine(); 107 $subject = ''; 108 $content = ''; 109 $default_subject = ''; 110 $default_content = ''; 111 112 if ($message_id > 0) { 113 $message = $this->getMessageById($message_id); 114 if ($message) { 115 $subject = $merge_engine->render($message['name'] ?? '', $context); 116 $content = $merge_engine->render($message['content'] ?? '', $context); 117 if ($submission_id > 0 && trim($content) === '') { 118 $this->log->add( 119 'warning', 120 'submission', 121 $submission_id, 122 0, 123 sprintf('Admin alert message %d has empty content; using default template.', $message_id), 124 $this->getMailLogCategory() 125 ); 126 } 127 } else { 128 if ($submission_id > 0) { 129 $this->log->add( 130 'warning', 131 'submission', 132 $submission_id, 133 0, 134 sprintf('Admin alert message %d no longer exists; using default template.', $message_id), 135 $this->getMailLogCategory() 136 ); 137 } 138 } 139 } 140 141 if ($subject === '' || trim($content) === '') { 142 $template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($template_data); 143 $default_subject = $merge_engine->render($template->getSubject(), $context); 144 $default_content = $merge_engine->render($template->getContent(), $context); 145 } 146 147 if ($subject === '') { 148 $subject = $default_subject; 149 } 150 151 if (trim($content) === '') { 152 $content = $default_content; 153 } 83 154 84 155 $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0)); 85 $mailer->setEmailSubject($ template->getSubject());86 $mailer->setEmailContent($ template->getContent());156 $mailer->setEmailSubject($subject); 157 $mailer->setEmailContent($content); 87 158 $mailer->setEmailRecipient($recipient); 88 159 $mailer->sendMail(); … … 93 164 $recipient = sanitize_email($payload['recipient'] ?? ''); 94 165 $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0; 166 $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0; 167 $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0; 95 168 if (empty($recipient) || $message_id <= 0) { 96 throw new \RuntimeException('Invalid autoresponder payload'); 169 $this->log->add( 170 'warning', 171 'submission', 172 $submission_id, 173 0, 174 'Autoresponder email ignored: invalid recipient or missing message mapping.', 175 $this->getMailLogCategory() 176 ); 177 return; 97 178 } 98 179 99 180 $message = $this->getMessageById($message_id); 100 181 if (!$message) { 101 throw new \RuntimeException(sprintf('Message %d not found', $message_id)); 182 $this->log->add( 183 'warning', 184 'submission', 185 $submission_id, 186 0, 187 sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id), 188 $this->getMailLogCategory() 189 ); 190 return; 191 } 192 193 $context = SubmissionContextBuilder::getInstance()->build((int) $job['submission_id']); 194 $form = $this->loadForm((int) ($job['form_id'] ?? 0)); 195 $context['form_object'] = $form; 196 if ($submission_id > 0 && (empty($context) || empty($context['submission']))) { 197 $this->log->add( 198 'warning', 199 'submission', 200 $submission_id, 201 0, 202 'Merge context missing for autoresponder; tokens may not resolve.', 203 $this->getMailLogCategory() 204 ); 205 } 206 $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine(); 207 $subject = $merge_engine->render($message['name'] ?? '', $context); 208 $content = $merge_engine->render($message['content'] ?? '', $context); 209 if (trim($content) === '') { 210 $this->log->add( 211 'warning', 212 'submission', 213 $submission_id, 214 0, 215 sprintf('Autoresponder message %d has empty content; email not sent.', $message_id), 216 $this->getMailLogCategory() 217 ); 218 return; 102 219 } 103 220 104 221 $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0)); 105 $mailer->setEmailSubject($ message['name'] ?? '');106 $mailer->setEmailContent($ message['content'] ?? '');222 $mailer->setEmailSubject($subject); 223 $mailer->setEmailContent($content); 107 224 $mailer->setEmailRecipient($recipient); 108 225 $mailer->sendMail(); … … 169 286 ); 170 287 } 288 289 private function getMailLogCategory(): string 290 { 291 if (!empty($this->mail_log_category)) { 292 return $this->mail_log_category; 293 } 294 295 $registry = \GriffinForms\Includes\Integrations\Categories\Mailer::getInstance(); 296 $provider = $registry ? $registry->getActiveProviderId() : 'server_mail'; 297 if (empty($provider)) { 298 $provider = 'server_mail'; 299 } 300 301 $this->mail_log_category = sprintf('Mail Integration (%s)', $provider); 302 return $this->mail_log_category; 303 } 171 304 } -
griffinforms-form-builder/trunk/installdata.php
r3421663 r3433300 22 22 $this->addDefaultLogSettings(); 23 23 $this->addTemplatesToDb(); 24 $this->addEmailTemplatesToDb(); 24 25 $this->addDefaultFormThemes(); 25 26 } … … 124 125 } 125 126 127 public function addEmailTemplatesToDb($force_reimport = false) 128 { 129 global $wpdb; 130 131 $table = $this->config->getTable('message'); 132 $templates_dir = $this->config->getRootPath() . 'admin/email_templates/'; 133 134 if (!is_dir($templates_dir)) { 135 return; 136 } 137 138 $template_files = glob($templates_dir . '*.json'); 139 if (empty($template_files)) { 140 return; 141 } 142 143 $inserted = 0; 144 $updated = 0; 145 $skipped = 0; 146 147 foreach ($template_files as $file) { 148 $template_data = json_decode(file_get_contents($file), true); 149 if (!is_array($template_data)) { 150 $this->log->add('error', 'message', 0, 0, 'Email template import failed: Invalid JSON in ' . basename($file), 'message'); 151 $skipped++; 152 continue; 153 } 154 155 $name = sanitize_text_field($template_data['name'] ?? ''); 156 if ($name === '') { 157 $this->log->add('error', 'message', 0, 0, 'Email template import skipped: Missing name in ' . basename($file), 'message'); 158 $skipped++; 159 continue; 160 } 161 162 $heading = sanitize_text_field($template_data['heading'] ?? ''); 163 $description = sanitize_text_field($template_data['description'] ?? ''); 164 $content = $template_data['content'] ?? ''; 165 $content = is_string($content) ? wp_kses_post($content) : ''; 166 167 $existing_template = $wpdb->get_row( 168 $wpdb->prepare("SELECT id FROM %i WHERE name = %s", $table, $name) 169 ); 170 171 $data = array( 172 'name' => $name, 173 'heading' => $heading, 174 'description' => $description, 175 'content' => $content, 176 'last_editor' => 0, 177 'last_edited' => current_time('mysql', true), 178 ); 179 180 if ($existing_template) { 181 if ($force_reimport) { 182 $wpdb->update($table, $data, array('id' => $existing_template->id)); 183 $updated++; 184 } else { 185 $skipped++; 186 } 187 continue; 188 } 189 190 $data['author'] = 0; 191 $data['created'] = current_time('mysql', true); 192 193 $wpdb->insert($table, $data); 194 $inserted++; 195 } 196 197 $this->log->add('info', 'message', 0, 0, "Email template installation summary: Inserted $inserted, Updated $updated, Skipped $skipped", 'message'); 198 } 199 126 200 /** 127 201 * Import default FormTheme templates from admin/themes directory on activation. -
griffinforms-form-builder/trunk/readme.txt
r3425584 r3433300 370 370 == Changelog == 371 371 372 = 2.1.3.0 – 2026-01-06 = 373 * Feature: Admin alert emails can map each recipient to a message template with a default fallback. 374 * Feature: Message editor now includes a merge-token builder and JSON placeholder insertion. 375 * Feature: Merge tokens resolve at send time for admin alerts and autoresponders. 376 * Improvement: Added plain-text autoresponder and admin alert starter templates. 377 * Improvement: Mail-related logging now captures mapping and merge edge cases at the submission level. 378 372 379 = 2.1.2.0 – 2025-12-22 = 373 380 * Fix: File upload queued list no longer shifts due to theme list padding. … … 651 658 == Upgrade Notice == 652 659 660 = 2.1.3.0 = 661 Admin alerts can now use per-recipient message templates with mail-merge placeholders, and the message editor includes a token builder. Includes new starter templates and improved mail logging. Recommended update. 662 653 663 = 2.1.2.0 = 654 664 Polish release focused on layout and clarity: file upload lists now ignore theme padding, the builder summary stays out of the way during drag‑and‑drop, attachments headers align, and long settings history values are easier to read. Logging labels for mail and payment are clearer and more consistent. Recommended update.
Note: See TracChangeset
for help on using the changeset viewer.