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