PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 All 139 releases
← All changes | includes/Core/WorkFlow/Helper.php +312 -45 3.1.4 → V3.3.2 View file →
@@ -2,11 +2,13 @@
2 2
3 3 namespace BitCode\BitForm\Core\WorkFlow;
4 4
5 5 use BitCode\BitForm\Core\Integration\IntegrationHandler;
6 +use BitCode\BitForm\Core\Messages\EmailTemplateHandler;
6 7 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
7 8 use BitCode\BitForm\Core\Util\FieldValueHandler;
8 9 use BitCode\BitForm\Core\Util\SmartTags;
10 +use BitCode\BitForm\Core\Util\Utilities;
9 11
10 12 final class Helper
11 13 {
12 14 public static function getFieldData($fields)
@@ -19,8 +21,12 @@
19 21 'key' => $fieldKey,
20 22 'value' => $value,
21 23 'type' => $fieldDetail->typ,
22 24 ];
25 + if ('advanced-datetime' === $fieldData[$fieldKey]['type']) {
26 + // Date conditions must parse the posted value in the field's own format.
27 + $fieldData[$fieldKey]['dateFormat'] = DateConditionComparator::advancedValueFormat(isset($fieldDetail->config) ? $fieldDetail->config : null);
28 + }
23 29 if (isset($fieldDetail->mul)) {
24 30 $fieldData[$fieldKey] =
25 31 array_merge(
26 32 $fieldData[$fieldKey],
@@ -28,8 +34,13 @@
28 34 'mul' => $fieldDetail->mul,
29 35 ]
30 36 );
31 37 }
38 + // A multiple-choice Image Select holds a list, like a checkbox; compared as one string
39 + // only its last choice counted.
40 + if ('image-select' === $fieldDetail->typ && isset($fieldDetail->inpType) && 'checkbox' === $fieldDetail->inpType) {
41 + $fieldData[$fieldKey]['mul'] = true;
42 + }
32 43 }
33 44 return $fieldData;
34 45 }
35 46
@@ -55,11 +66,11 @@
55 66 }
56 67 return $fieldData;
57 68 }
58 69
59 - public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null)
70 + public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null, $stripShortcodesFromValues = false)
60 71 {
61 - $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID);
72 + $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID, $stripShortcodesFromValues);
62 73 if ($evalMathExpr) {
63 74 return self::evalMathExpression($stringToReplaceField);
64 75 }
65 76 return $stringToReplaceField;
@@ -64,42 +75,74 @@
64 75 }
65 76 return $stringToReplaceField;
66 77 }
67 78
68 - public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0)
79 + public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0, $workFlowRun = null)
69 80 {
70 81 $messageId = 0;
71 82 $returnableData = null;
72 - return [
73 - 'msg_id' => $messageId,
74 - 'confirmation' => $returnableData,
75 - ];
83 + $afterSubmit = null;
84 + $msgDuration = null;
76 85 $integrationHandler = new IntegrationHandler($formId);
86 + $conditionalActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
77 87 switch ($confirmationType) {
78 88 case 'successMsg':
79 - $successMessageHandler
80 - = new SuccessMessageHandler($formId);
89 + // Standalone confirmation: show the first ENABLED success message without requiring a workflow.
90 + $successMessageHandler = new SuccessMessageHandler($formId);
81 91 $successMessage = $successMessageHandler->getAllMessage();
82 - if (!is_wp_error($successMessage) && !empty($successMessage) && count($successMessage) > 0) {
83 - $returnableData = self::replaceFieldWithValue($successMessage[0]->message_content, $fieldValue);
84 - $messageId = $successMessage[0]->id;
92 + if (!is_wp_error($successMessage) && !empty($successMessage)) {
93 + foreach ($successMessage as $msg) {
94 + $msgConfig = Utilities::jsonObj($msg->message_config);
95 + if (
96 + isset($msgConfig->status) && empty($msgConfig->status)
97 + || isset($conditionalActionIds['successMsg'][(string) $msg->id])
98 + ) {
99 + continue;
100 + }
101 + // Translate before smart-tag replacement (identity when unhooked).
102 + $messageContent = (string) apply_filters(
103 + 'bitform_translate_form_string',
104 + (string) $msg->message_content,
105 + 'msg-content-' . $msg->id,
106 + $formId
107 + );
108 + $returnableData = self::replaceFieldWithValue($messageContent, $fieldValue, true, null, true);
109 + $messageId = $msg->id;
110 + if (isset($msgConfig->afterSubmit)) {
111 + $afterSubmit = $msgConfig->afterSubmit;
112 + }
113 + if (!empty($msgConfig->autoHide)) {
114 + $msgDuration = abs(floatval($msgConfig->duration ?? 0) * 1000);
115 + }
116 + break;
117 + }
85 118 }
86 119 break;
87 120 case 'redirectPage':
88 - $redirectPage = $integrationHandler->getAllIntegration('form', 'redirectPage');
89 - if (!is_wp_error($redirectPage) && !empty($redirectPage) && count($redirectPage) > 0) {
90 - $url = json_decode($redirectPage[0]->integration_details)->url;
91 - if (!empty($url)) {
92 - $url = self::replaceFieldWithValue($url, $fieldValue);
121 + $redirectPages = $integrationHandler->getAllIntegration('form', 'redirectPage', 1);
122 + if (!is_wp_error($redirectPages) && !empty($redirectPages)) {
123 + foreach ($redirectPages as $redirectPage) {
124 + if (isset($conditionalActionIds['redirectPage'][(string) $redirectPage->id])) {
125 + continue;
126 + }
127 + $url = Utilities::jsonObj($redirectPage->integration_details ?? '')->url ?? '';
128 + if (!empty($url)) {
129 + // Translated before smart-tag replacement: per-language redirect targets.
130 + $url = (string) apply_filters(
131 + 'bitform_translate_form_string',
132 + (string) $url,
133 + 'redirect-url-' . $redirectPage->id,
134 + $formId
135 + );
136 + $url = self::replaceFieldWithValue($url, $fieldValue);
137 + }
138 + $returnableData = empty($url) ? '' : esc_url_raw($url);
139 + break;
93 140 }
94 - $returnableData = empty($url) ? '' : esc_url_raw($url);
95 141 }
96 142 break;
97 143 case 'webHooks':
98 - $webHooks = $integrationHandler->getAllIntegration('form', 'webHooks');
99 - if (!is_wp_error($webHooks) && !empty($webHooks) && 1 === count($webHooks)) {
100 - $returnableData = ["{\"id\":{$webHooks[0]->id}}"];
101 - }
144 + // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases).
102 145 break;
103 146 default:
104 147 break;
105 148 }
@@ -106,25 +149,193 @@
106 149
107 150 return [
108 151 'msg_id' => $messageId,
109 152 'confirmation' => $returnableData,
153 + 'afterSubmit' => $afterSubmit,
154 + 'msg_duration' => $msgDuration,
110 155 ];
111 156 }
112 157
158 + private static function getDefaultSubmitExcludedActionIds($formId, $workFlowRun = null)
159 + {
160 + $actionIds = [
161 + 'successMsg' => [],
162 + 'redirectPage' => [],
163 + 'mailNotify' => [],
164 + 'integrations' => [],
165 + ];
166 + $filteredActionIds = apply_filters(
167 + 'bitform_default_submit_confirmation_excluded_action_ids',
168 + $actionIds,
169 + $formId,
170 + $workFlowRun
171 + );
172 +
173 + return is_array($filteredActionIds) ? $filteredActionIds : $actionIds;
174 + }
175 +
176 + public static function getDefaultMailNotifications($formId, $workFlowRun = null)
177 + {
178 + $mailData = [];
179 + $emailTemplateHandler = new EmailTemplateHandler($formId);
180 + $templates = $emailTemplateHandler->getAllTemplate();
181 + if (empty($templates) || is_wp_error($templates)) {
182 + return $mailData;
183 + }
184 + $excludedActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
185 + $arrayFields = ['to', 'cc', 'bcc', 'replyto', 'attachment', 'mediaAttachment', 'pdfIds'];
186 + foreach ($templates as $template) {
187 + $status = isset($template->status) ? (int) $template->status : 1;
188 + if (
189 + 1 !== $status
190 + || isset($excludedActionIds['mailNotify'][(string) $template->id])
191 + ) {
192 + continue;
193 + }
194 + $config = json_decode($template->config);
195 + $details = new \stdClass();
196 + $details->id = wp_json_encode(['id' => (string) $template->id]);
197 + foreach (['to', 'from', 'from_name', 'cc', 'bcc', 'replyto', 'attachment', 'mediaAttachment', 'pdfId', 'pdfIds'] as $key) {
198 + $details->$key = isset($config->$key) ? $config->$key : (in_array($key, $arrayFields, true) ? [] : '');
199 + }
200 + $mailData[] = $details;
201 + }
202 + return $mailData;
203 + }
204 +
205 + public static function getDefaultIntegrations($formId, $workFlowRun = null)
206 + {
207 + $integrationIds = [];
208 + $integrationHandler = new IntegrationHandler($formId);
209 + $allIntegrations = $integrationHandler->getAllIntegration('form', null, 1);
210 + if (empty($allIntegrations) || is_wp_error($allIntegrations)) {
211 + return $integrationIds;
212 + }
213 + $excludedActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
214 + foreach ($allIntegrations as $integration) {
215 + if (
216 + 'redirectPage' === $integration->integration_type
217 + || isset($excludedActionIds['integrations'][(string) $integration->id])
218 + ) {
219 + continue;
220 + }
221 + // triggerData['integrations'] is a list of GROUPS; each group is an array of JSON id strings
222 + // (Integrations::executeIntegrations requires is_array() with string members). Wrap each id.
223 + $integrationIds[] = [wp_json_encode(['id' => (string) $integration->id])];
224 + }
225 + return $integrationIds;
226 + }
227 +
228 + /**
229 + * Free implementation of the default-submit exclusion filter: any message / redirect / email /
230 + * integration referenced by a classic/basic onsubmit workflow success action is workflow-gated,
231 + * so it must NOT be default-executed (Pro adds the advanced-CL ids separately). Registered in Hooks.
232 + */
233 + public static function workflowReferencedActionIds($actionIds, $formId, $workFlowRun = null)
234 + {
235 + static $cache = [];
236 +
237 + $actionIds = is_array($actionIds) ? $actionIds : [];
238 + foreach (['successMsg', 'redirectPage', 'mailNotify', 'integrations'] as $bucket) {
239 + if (empty($actionIds[$bucket]) || !is_array($actionIds[$bucket])) {
240 + $actionIds[$bucket] = [];
241 + }
242 + }
243 +
244 + $cacheKey = (string) $formId . ':' . (string) $workFlowRun;
245 + if (!isset($cache[$cacheKey])) {
246 + $workFlow = new WorkFlow($formId);
247 + $rows = $workFlow->getWorkFlow(['create_edit', $workFlowRun ?? 'create'], ['onsubmit'], null, 'workflow_order');
248 + $cache[$cacheKey] = self::collectReferencedActionIds(is_wp_error($rows) ? [] : $rows);
249 + }
250 +
251 + foreach ($cache[$cacheKey] as $bucket => $ids) {
252 + $actionIds[$bucket] = $actionIds[$bucket] + $ids;
253 + }
254 + return $actionIds;
255 + }
256 +
257 + /**
258 + * Extract the message / redirect / email / integration ids referenced by workflow-row success
259 + * actions, grouped into the four exclusion buckets. Shared by the runtime default-submit filter
260 + * and the one-time orphan-deactivation migration (SubmitActionFallback).
261 + *
262 + * @param array $rows workflow rows each having a ->workflow_condition JSON string
263 + *
264 + * @return array{successMsg:array,redirectPage:array,mailNotify:array,integrations:array}
265 + */
266 + public static function collectReferencedActionIds($rows)
267 + {
268 + $referenced = ['successMsg' => [], 'redirectPage' => [], 'mailNotify' => [], 'integrations' => []];
269 + if (empty($rows) || !is_array($rows)) {
270 + return $referenced;
271 + }
272 + foreach ($rows as $row) {
273 + $conditions = json_decode($row->workflow_condition ?? '');
274 + if (empty($conditions) || !is_array($conditions)) {
275 + continue;
276 + }
277 + foreach ($conditions as $condition) {
278 + if (empty($condition->actions->success)) {
279 + continue;
280 + }
281 + foreach ($condition->actions->success as $success) {
282 + if (empty($success->type) || empty($success->details->id)) {
283 + continue;
284 + }
285 + $bucket = self::actionExclusionBucket($success->type);
286 + if (null === $bucket) {
287 + continue;
288 + }
289 + foreach (self::extractActionIds($success->details->id) as $id) {
290 + $referenced[$bucket][$id] = true;
291 + }
292 + }
293 + }
294 + }
295 + return $referenced;
296 + }
297 +
298 + private static function actionExclusionBucket($type)
299 + {
300 + switch ($type) {
301 + case 'successMsg':
302 + return 'successMsg';
303 + case 'redirectPage':
304 + return 'redirectPage';
305 + case 'mailNotify':
306 + return 'mailNotify';
307 + case 'integ':
308 + case 'webHooks':
309 + return 'integrations';
310 + default:
311 + return null;
312 + }
313 + }
314 +
315 + private static function extractActionIds($detailId)
316 + {
317 + $ids = [];
318 + $items = is_array($detailId) ? $detailId : [$detailId];
319 + foreach ($items as $item) {
320 + if (!is_string($item)) {
321 + continue;
322 + }
323 + $decoded = json_decode($item);
324 + if (isset($decoded->id)) {
325 + $ids[] = (string) $decoded->id;
326 + }
327 + }
328 + return $ids;
329 + }
330 +
331 + /**
332 + * @deprecated misspelled duplicate of calculate(); kept because it is public API. Same
333 + * divide-by-zero guard so an external caller cannot fatal either.
334 + */
113 335 public static function calculte($firstOperand, $secondOperand, $operator)
114 336 {
115 - switch ($operator) {
116 - case '+':
117 - return $firstOperand + $secondOperand;
118 - case '-':
119 - return $firstOperand - $secondOperand;
120 - case '*':
121 - return $firstOperand * $secondOperand;
122 - case '/':
123 - return $firstOperand / $secondOperand;
124 - case '^':
125 - return $firstOperand ** $secondOperand;
126 - }
337 + return self::calculate($firstOperand, $secondOperand, $operator);
127 338 }
128 339
129 340 public static function filterMailContentType()
130 341 {
@@ -133,11 +344,26 @@
133 344
134 345 public static function evalMathExpression($stringWithFieldValue)
135 346 {
136 347 $mathExpr = $stringWithFieldValue;
137 - if (empty($mathExpr)) {
348 + if (empty($mathExpr) || !\is_scalar($mathExpr)) {
138 349 return $stringWithFieldValue;
139 350 }
351 + $mathExpr = (string) $mathExpr;
352 +
353 + // The operand/operator checks below only look at \w+ runs and operator runs, so any other
354 + // character was invisible to them. A quoted date ('2020-10-10') therefore passed as a
355 + // subtraction chain and its quotes later surfaced as a bogus operator token. Require the whole
356 + // string to be made of things a formula can contain.
357 + if (1 !== preg_match('#^[0-9.+\-*/^()\[\]{}\s]+$#', $mathExpr)) {
358 + return $stringWithFieldValue;
359 + }
360 +
361 + // A bare date is not a subtraction: 2020-10-10 must stay a date, not become 2000.
362 + if (1 === preg_match('/^\s*\d{4}-\d{1,2}-\d{1,2}\s*$/', $mathExpr)) {
363 + return $stringWithFieldValue;
364 + }
365 +
140 366 preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr);
141 367 if (empty($isMathExpr[0])) {
142 368 return $stringWithFieldValue;
143 369 }
@@ -159,9 +385,9 @@
159 385 $mathExpr = str_replace(' ', '', $mathExpr);
160 386 $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr);
161 387 $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr);
162 388 $calculated = self::infixToPostfixEvalute($mathExpr);
163 - if (!is_null($calculated)) {
389 + if (!is_null($calculated) && isset($calculated[0])) {
164 390 return (string) $calculated[0];
165 391 }
166 392
167 393 return (string) $stringWithFieldValue;
@@ -185,10 +411,18 @@
185 411 }
186 412 if ('(' === $token) {
187 413 $operatorStack[] = $token;
188 414 } elseif (')' === $token) {
415 + // An unbalanced ')' used to read $operatorStack[-1] and warn on every iteration; treat
416 + // the expression as non-arithmetic instead.
417 + if (empty($operatorStack)) {
418 + return null;
419 + }
189 420 while ('(' !== $operatorStack[count($operatorStack) - 1]) {
190 421 $outputQueue[] = array_pop($operatorStack);
422 + if (empty($operatorStack)) {
423 + return null;
424 + }
191 425 if ('(' === $operatorStack[count($operatorStack) - 1]) {
192 426 array_pop($operatorStack);
193 427 break;
194 428 }
@@ -217,11 +451,22 @@
217 451 if (is_numeric($value)) {
218 452 $resultStack[] = $value;
219 453 continue;
220 454 }
455 + // A token that is neither a number nor a real operator means the input was never an
456 + // expression — e.g. a quoted date whose quotes accumulated into a token like "'2020".
457 + // Bail out so evalMathExpression() returns the caller's string untouched; pushing the
458 + // failure onto the stack would end up blanking that string.
459 + if (!\in_array($value, ['+', '-', '*', '/', '^'], true) || count($resultStack) < 2) {
460 + return null;
461 + }
221 462 $secondOperand = array_pop($resultStack);
222 463 $firstOperand = array_pop($resultStack);
223 - $resultStack[] = self::calculate($firstOperand, $secondOperand, $value);
464 + $calculated = self::calculate($firstOperand, $secondOperand, $value);
465 + if (\is_null($calculated)) {
466 + return null;
467 + }
468 + $resultStack[] = $calculated;
224 469 }
225 470 return $resultStack;
226 471 }
227 472
@@ -237,19 +482,41 @@
237 482
238 483 return isset($precedence[$operator]) ? $precedence[$operator] : 0;
239 484 }
240 485
486 + /**
487 + * Apply one arithmetic operator.
488 + *
489 + * @param mixed $firstOperand
490 + * @param mixed $secondOperand
491 + * @param string $operator
492 + *
493 + * @return float|int|null null on unknown operator, non-numeric operand, or division by zero
494 + */
241 495 public static function calculate($firstOperand, $secondOperand, $operator)
242 496 {
243 - $calculated = [
244 - '+' => $firstOperand + $secondOperand,
245 - '-' => $firstOperand - $secondOperand,
246 - '*' => $firstOperand * $secondOperand,
247 - '/' => $firstOperand / $secondOperand,
248 - '^' => $firstOperand ** $secondOperand,
249 - ];
497 + if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) {
498 + return null;
499 + }
500 + $firstOperand = $firstOperand + 0;
501 + $secondOperand = $secondOperand + 0;
250 502
251 - return isset($calculated[$operator]) ? $calculated[$operator] : '';
503 + switch ($operator) {
504 + case '+':
505 + return $firstOperand + $secondOperand;
506 + case '-':
507 + return $firstOperand - $secondOperand;
508 + case '*':
509 + return $firstOperand * $secondOperand;
510 + case '/':
511 + // Compare as float, not with `0 ==`: PHP 8 made `0 == ''` false, so a
512 + // non-numeric operand slipped past the old guard into DivisionByZeroError.
513 + return 0.0 === (float) $secondOperand ? null : $firstOperand / $secondOperand;
514 + case '^':
515 + return $firstOperand ** $secondOperand;
516 + }
517 +
518 + return null;
252 519 }
253 520
254 521 /**
255 522 * Recursively sets a nested property in a given object.