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 +364 -56 2.0 → V3.3.2 View file →
@@ -2,36 +2,51 @@
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 -final class Helper {
11 - public static function getFieldData($fields) {
12 +final class Helper
13 +{
14 + public static function getFieldData($fields)
15 + {
12 16 $fieldData = [];
13 17
14 18 foreach ($fields as $fieldKey => $fieldDetail) {
19 + $value = isset($fieldDetail->val) ? $fieldDetail->val : (isset($fieldDetail->defaultValue) ? $fieldDetail->defaultValue : '');
15 20 $fieldData[$fieldKey] = [
16 21 'key' => $fieldKey,
17 - 'value' => empty($fieldDetail->val) ? '' : $fieldDetail->val,
22 + 'value' => $value,
18 23 'type' => $fieldDetail->typ,
19 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 + }
20 29 if (isset($fieldDetail->mul)) {
21 30 $fieldData[$fieldKey] =
22 31 array_merge(
23 32 $fieldData[$fieldKey],
24 33 [
25 - 'mul' => true,
34 + 'mul' => $fieldDetail->mul,
26 35 ]
27 36 );
28 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 + }
29 43 }
30 44 return $fieldData;
31 45 }
32 46
33 - public static function smartFldMargeFormFld($logics, $fieldData) {
47 + public static function smartFldMargeFormFld($logics, $fieldData)
48 + {
34 49 $fieldKeys = SmartTags::smartTagFieldKeys();
35 50
36 51 foreach ($logics as $logic) {
37 52 $removeSpecialStr = ['${', '}', '()'];
@@ -51,10 +66,11 @@
51 66 }
52 67 return $fieldData;
53 68 }
54 69
55 - public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true) {
56 - $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues);
70 + public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null, $stripShortcodesFromValues = false)
71 + {
72 + $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID, $stripShortcodesFromValues);
57 73 if ($evalMathExpr) {
58 74 return self::evalMathExpression($stringToReplaceField);
59 75 }
60 76 return $stringToReplaceField;
@@ -59,41 +75,74 @@
59 75 }
60 76 return $stringToReplaceField;
61 77 }
62 78
63 - public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0) {
79 + public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0, $workFlowRun = null)
80 + {
64 81 $messageId = 0;
65 82 $returnableData = null;
66 - return [
67 - 'msg_id' => $messageId,
68 - 'confirmation' => $returnableData,
69 - ];
83 + $afterSubmit = null;
84 + $msgDuration = null;
70 85 $integrationHandler = new IntegrationHandler($formId);
86 + $conditionalActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
71 87 switch ($confirmationType) {
72 88 case 'successMsg':
73 - $successMessageHandler
74 - = new SuccessMessageHandler($formId);
89 + // Standalone confirmation: show the first ENABLED success message without requiring a workflow.
90 + $successMessageHandler = new SuccessMessageHandler($formId);
75 91 $successMessage = $successMessageHandler->getAllMessage();
76 - if (!is_wp_error($successMessage) && !empty($successMessage) && count($successMessage) > 0) {
77 - $returnableData = self::replaceFieldWithValue($successMessage[0]->message_content, $fieldValue);
78 - $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 + }
79 118 }
80 119 break;
81 120 case 'redirectPage':
82 - $redirectPage = $integrationHandler->getAllIntegration('form', 'redirectPage');
83 - if (!is_wp_error($redirectPage) && !empty($redirectPage) && count($redirectPage) > 0) {
84 - $url = json_decode($redirectPage[0]->integration_details)->url;
85 - if (!empty($url)) {
86 - $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;
87 140 }
88 - $returnableData = empty($url) ? '' : esc_url_raw($url);
89 141 }
90 142 break;
91 143 case 'webHooks':
92 - $webHooks = $integrationHandler->getAllIntegration('form', 'webHooks');
93 - if (!is_wp_error($webHooks) && !empty($webHooks) && 1 === count($webHooks)) {
94 - $returnableData = ["{\"id\":{$webHooks[0]->id}}"];
95 - }
144 + // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases).
96 145 break;
97 146 default:
98 147 break;
99 148 }
@@ -100,35 +149,221 @@
100 149
101 150 return [
102 151 'msg_id' => $messageId,
103 152 'confirmation' => $returnableData,
153 + 'afterSubmit' => $afterSubmit,
154 + 'msg_duration' => $msgDuration,
104 155 ];
105 156 }
106 157
107 - public static function calculte($firstOperand, $secondOperand, $operator) {
108 - switch ($operator) {
109 - case '+':
110 - return $firstOperand + $secondOperand;
111 - case '-':
112 - return $firstOperand - $secondOperand;
113 - case '*':
114 - return $firstOperand * $secondOperand;
115 - case '/':
116 - return $firstOperand / $secondOperand;
117 - case '^':
118 - return $firstOperand ** $secondOperand;
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;
119 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;
120 203 }
121 204
122 - public static function filterMailContentType() {
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 + */
335 + public static function calculte($firstOperand, $secondOperand, $operator)
336 + {
337 + return self::calculate($firstOperand, $secondOperand, $operator);
338 + }
339 +
340 + public static function filterMailContentType()
341 + {
123 342 return 'text/html';
124 343 }
125 344
126 - public static function evalMathExpression($stringWithFieldValue) {
345 + public static function evalMathExpression($stringWithFieldValue)
346 + {
127 347 $mathExpr = $stringWithFieldValue;
128 - if (empty($mathExpr)) {
348 + if (empty($mathExpr) || !\is_scalar($mathExpr)) {
129 349 return $stringWithFieldValue;
130 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 +
131 366 preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr);
132 367 if (empty($isMathExpr[0])) {
133 368 return $stringWithFieldValue;
134 369 }
@@ -146,13 +381,13 @@
146 381 if (!in_array(trim($value), $validOperator)) {
147 382 return $stringWithFieldValue;
148 383 }
149 384 }
150 - $mathExpr = str_replace(' ', null, $mathExpr);
385 + $mathExpr = str_replace(' ', '', $mathExpr);
151 386 $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr);
152 387 $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr);
153 388 $calculated = self::infixToPostfixEvalute($mathExpr);
154 - if (!is_null($calculated)) {
389 + if (!is_null($calculated) && isset($calculated[0])) {
155 390 return (string) $calculated[0];
156 391 }
157 392
158 393 return (string) $stringWithFieldValue;
@@ -157,9 +392,10 @@
157 392
158 393 return (string) $stringWithFieldValue;
159 394 }
160 395
161 - public static function infixToPostfixEvalute($expression) {
396 + public static function infixToPostfixEvalute($expression)
397 + {
162 398 $operatorStack = [];
163 399 $outputQueue = [];
164 400 $numTemp = null;
165 401 for ($strIndex = 0; $strIndex < strlen($expression); $strIndex++) {
@@ -175,10 +411,18 @@
175 411 }
176 412 if ('(' === $token) {
177 413 $operatorStack[] = $token;
178 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 + }
179 420 while ('(' !== $operatorStack[count($operatorStack) - 1]) {
180 421 $outputQueue[] = array_pop($operatorStack);
422 + if (empty($operatorStack)) {
423 + return null;
424 + }
181 425 if ('(' === $operatorStack[count($operatorStack) - 1]) {
182 426 array_pop($operatorStack);
183 427 break;
184 428 }
@@ -207,16 +451,28 @@
207 451 if (is_numeric($value)) {
208 452 $resultStack[] = $value;
209 453 continue;
210 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 + }
211 462 $secondOperand = array_pop($resultStack);
212 463 $firstOperand = array_pop($resultStack);
213 - $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;
214 469 }
215 470 return $resultStack;
216 471 }
217 472
218 - public static function operatorPrecedence($operator) {
473 + public static function operatorPrecedence($operator)
474 + {
219 475 $precedence = [
220 476 '+' => 2,
221 477 '-' => 2,
222 478 '*' => 3,
@@ -226,16 +482,68 @@
226 482
227 483 return isset($precedence[$operator]) ? $precedence[$operator] : 0;
228 484 }
229 485
230 - public static function calculate($firstOperand, $secondOperand, $operator) {
231 - $calculated = [
232 - '+' => $firstOperand + $secondOperand,
233 - '-' => $firstOperand - $secondOperand,
234 - '*' => $firstOperand * $secondOperand,
235 - '/' => $firstOperand / $secondOperand,
236 - '^' => $firstOperand ** $secondOperand,
237 - ];
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 + */
495 + public static function calculate($firstOperand, $secondOperand, $operator)
496 + {
497 + if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) {
498 + return null;
499 + }
500 + $firstOperand = $firstOperand + 0;
501 + $secondOperand = $secondOperand + 0;
238 502
239 - 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;
519 + }
520 +
521 + /**
522 + * Recursively sets a nested property in a given object.
523 + *
524 + * This function takes an object, a string path representing nested properties
525 + * (e.g., "fk->valid->hide"), and a value to assign. It ensures that all
526 + * intermediate properties exist as objects before setting the final value.
527 + *
528 + * @param object $object The main object where properties should be set.
529 + * @param string $path The nested property path, with keys separated by "->".
530 + * @param mixed $value The value to assign to the final property.
531 + *
532 + * @return void
533 + */
534 + public static function setNestedProperty(&$object, $path, $value)
535 + {
536 + $keys = explode('->', $path);
537 + $key = array_shift($keys);
538 +
539 + if (!isset($object->$key) || !is_object($object->$key)) {
540 + $object->$key = new \stdClass();
541 + }
542 +
543 + if (!empty($keys)) {
544 + self::setNestedProperty($object->$key, implode('->', $keys), $value);
545 + } else {
546 + $object->$key = $value;
547 + }
240 548 }
241 549 }