PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.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 2.10.2 All 137 releases
bit-form / includes / Core / WorkFlow / Helper.php

Helper.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.2.1, at includes/Core/WorkFlow/Helper.php

525 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Core\WorkFlow;
4
5 use BitCode\BitForm\Core\Integration\IntegrationHandler;
6 use BitCode\BitForm\Core\Messages\EmailTemplateHandler;
7 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
8 use BitCode\BitForm\Core\Util\FieldValueHandler;
9 use BitCode\BitForm\Core\Util\SmartTags;
10 use BitCode\BitForm\Core\Util\Utilities;
11
12 final class Helper
13 {
14 public static function getFieldData($fields)
15 {
16 $fieldData = [];
17
18 foreach ($fields as $fieldKey => $fieldDetail) {
19 $value = isset($fieldDetail->val) ? $fieldDetail->val : (isset($fieldDetail->defaultValue) ? $fieldDetail->defaultValue : '');
20 $fieldData[$fieldKey] = [
21 'key' => $fieldKey,
22 'value' => $value,
23 'type' => $fieldDetail->typ,
24 ];
25 if (isset($fieldDetail->mul)) {
26 $fieldData[$fieldKey] =
27 array_merge(
28 $fieldData[$fieldKey],
29 [
30 'mul' => $fieldDetail->mul,
31 ]
32 );
33 }
34 }
35 return $fieldData;
36 }
37
38 public static function smartFldMargeFormFld($logics, $fieldData)
39 {
40 $fieldKeys = SmartTags::smartTagFieldKeys();
41
42 foreach ($logics as $logic) {
43 $removeSpecialStr = ['${', '}', '()'];
44 if (isset($logic->field) && in_array(str_replace($removeSpecialStr, '', $logic->field), $fieldKeys)) {
45 $field = str_replace($removeSpecialStr, '', $logic->field);
46
47 $fldKey = '${' . $field . '}';
48 $customValue = isset($logic->smartKey) ? $logic->smartKey : '';
49 $val = SmartTags::getSmartTagValue($field, false, $customValue);
50
51 $fieldData[$fldKey] = [
52 'key' => $fldKey,
53 'value' => empty($val) ? '' : $val,
54 'type' => is_string($val) ? 'text' : 'array',
55 ];
56 }
57 }
58 return $fieldData;
59 }
60
61 public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null, $stripShortcodesFromValues = false)
62 {
63 $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID, $stripShortcodesFromValues);
64 if ($evalMathExpr) {
65 return self::evalMathExpression($stringToReplaceField);
66 }
67 return $stringToReplaceField;
68 }
69
70 public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0, $workFlowRun = null)
71 {
72 $messageId = 0;
73 $returnableData = null;
74 $afterSubmit = null;
75 $msgDuration = null;
76 $integrationHandler = new IntegrationHandler($formId);
77 $conditionalActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
78 switch ($confirmationType) {
79 case 'successMsg':
80 // Standalone confirmation: show the first ENABLED success message without requiring a workflow.
81 $successMessageHandler = new SuccessMessageHandler($formId);
82 $successMessage = $successMessageHandler->getAllMessage();
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 $returnableData = self::replaceFieldWithValue($msg->message_content, $fieldValue, true, null, true);
93 $messageId = $msg->id;
94 if (isset($msgConfig->afterSubmit)) {
95 $afterSubmit = $msgConfig->afterSubmit;
96 }
97 if (!empty($msgConfig->autoHide)) {
98 $msgDuration = abs(floatval($msgConfig->duration ?? 0) * 1000);
99 }
100 break;
101 }
102 }
103 break;
104 case 'redirectPage':
105 $redirectPages = $integrationHandler->getAllIntegration('form', 'redirectPage', 1);
106 if (!is_wp_error($redirectPages) && !empty($redirectPages)) {
107 foreach ($redirectPages as $redirectPage) {
108 if (isset($conditionalActionIds['redirectPage'][(string) $redirectPage->id])) {
109 continue;
110 }
111 $url = Utilities::jsonObj($redirectPage->integration_details ?? '')->url ?? '';
112 if (!empty($url)) {
113 $url = self::replaceFieldWithValue($url, $fieldValue);
114 }
115 $returnableData = empty($url) ? '' : esc_url_raw($url);
116 break;
117 }
118 }
119 break;
120 case 'webHooks':
121 // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases).
122 break;
123 default:
124 break;
125 }
126
127 return [
128 'msg_id' => $messageId,
129 'confirmation' => $returnableData,
130 'afterSubmit' => $afterSubmit,
131 'msg_duration' => $msgDuration,
132 ];
133 }
134
135 private static function getDefaultSubmitExcludedActionIds($formId, $workFlowRun = null)
136 {
137 $actionIds = [
138 'successMsg' => [],
139 'redirectPage' => [],
140 'mailNotify' => [],
141 'integrations' => [],
142 ];
143 $filteredActionIds = apply_filters(
144 'bitform_default_submit_confirmation_excluded_action_ids',
145 $actionIds,
146 $formId,
147 $workFlowRun
148 );
149
150 return is_array($filteredActionIds) ? $filteredActionIds : $actionIds;
151 }
152
153 public static function getDefaultMailNotifications($formId, $workFlowRun = null)
154 {
155 $mailData = [];
156 $emailTemplateHandler = new EmailTemplateHandler($formId);
157 $templates = $emailTemplateHandler->getAllTemplate();
158 if (empty($templates) || is_wp_error($templates)) {
159 return $mailData;
160 }
161 $excludedActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
162 $arrayFields = ['to', 'cc', 'bcc', 'replyto', 'attachment', 'mediaAttachment', 'pdfIds'];
163 foreach ($templates as $template) {
164 $status = isset($template->status) ? (int) $template->status : 1;
165 if (
166 1 !== $status
167 || isset($excludedActionIds['mailNotify'][(string) $template->id])
168 ) {
169 continue;
170 }
171 $config = json_decode($template->config);
172 $details = new \stdClass();
173 $details->id = wp_json_encode(['id' => (string) $template->id]);
174 foreach (['to', 'from', 'from_name', 'cc', 'bcc', 'replyto', 'attachment', 'mediaAttachment', 'pdfId', 'pdfIds'] as $key) {
175 $details->$key = isset($config->$key) ? $config->$key : (in_array($key, $arrayFields, true) ? [] : '');
176 }
177 $mailData[] = $details;
178 }
179 return $mailData;
180 }
181
182 public static function getDefaultIntegrations($formId, $workFlowRun = null)
183 {
184 $integrationIds = [];
185 $integrationHandler = new IntegrationHandler($formId);
186 $allIntegrations = $integrationHandler->getAllIntegration('form', null, 1);
187 if (empty($allIntegrations) || is_wp_error($allIntegrations)) {
188 return $integrationIds;
189 }
190 $excludedActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
191 foreach ($allIntegrations as $integration) {
192 if (
193 'redirectPage' === $integration->integration_type
194 || isset($excludedActionIds['integrations'][(string) $integration->id])
195 ) {
196 continue;
197 }
198 // triggerData['integrations'] is a list of GROUPS; each group is an array of JSON id strings
199 // (Integrations::executeIntegrations requires is_array() with string members). Wrap each id.
200 $integrationIds[] = [wp_json_encode(['id' => (string) $integration->id])];
201 }
202 return $integrationIds;
203 }
204
205 /**
206 * Free implementation of the default-submit exclusion filter: any message / redirect / email /
207 * integration referenced by a classic/basic onsubmit workflow success action is workflow-gated,
208 * so it must NOT be default-executed (Pro adds the advanced-CL ids separately). Registered in Hooks.
209 */
210 public static function workflowReferencedActionIds($actionIds, $formId, $workFlowRun = null)
211 {
212 static $cache = [];
213
214 $actionIds = is_array($actionIds) ? $actionIds : [];
215 foreach (['successMsg', 'redirectPage', 'mailNotify', 'integrations'] as $bucket) {
216 if (empty($actionIds[$bucket]) || !is_array($actionIds[$bucket])) {
217 $actionIds[$bucket] = [];
218 }
219 }
220
221 $cacheKey = (string) $formId . ':' . (string) $workFlowRun;
222 if (!isset($cache[$cacheKey])) {
223 $workFlow = new WorkFlow($formId);
224 $rows = $workFlow->getWorkFlow(['create_edit', $workFlowRun ?? 'create'], ['onsubmit'], null, 'workflow_order');
225 $cache[$cacheKey] = self::collectReferencedActionIds(is_wp_error($rows) ? [] : $rows);
226 }
227
228 foreach ($cache[$cacheKey] as $bucket => $ids) {
229 $actionIds[$bucket] = $actionIds[$bucket] + $ids;
230 }
231 return $actionIds;
232 }
233
234 /**
235 * Extract the message / redirect / email / integration ids referenced by workflow-row success
236 * actions, grouped into the four exclusion buckets. Shared by the runtime default-submit filter
237 * and the one-time orphan-deactivation migration (SubmitActionFallback).
238 *
239 * @param array $rows workflow rows each having a ->workflow_condition JSON string
240 *
241 * @return array{successMsg:array,redirectPage:array,mailNotify:array,integrations:array}
242 */
243 public static function collectReferencedActionIds($rows)
244 {
245 $referenced = ['successMsg' => [], 'redirectPage' => [], 'mailNotify' => [], 'integrations' => []];
246 if (empty($rows) || !is_array($rows)) {
247 return $referenced;
248 }
249 foreach ($rows as $row) {
250 $conditions = json_decode($row->workflow_condition ?? '');
251 if (empty($conditions) || !is_array($conditions)) {
252 continue;
253 }
254 foreach ($conditions as $condition) {
255 if (empty($condition->actions->success)) {
256 continue;
257 }
258 foreach ($condition->actions->success as $success) {
259 if (empty($success->type) || empty($success->details->id)) {
260 continue;
261 }
262 $bucket = self::actionExclusionBucket($success->type);
263 if (null === $bucket) {
264 continue;
265 }
266 foreach (self::extractActionIds($success->details->id) as $id) {
267 $referenced[$bucket][$id] = true;
268 }
269 }
270 }
271 }
272 return $referenced;
273 }
274
275 private static function actionExclusionBucket($type)
276 {
277 switch ($type) {
278 case 'successMsg':
279 return 'successMsg';
280 case 'redirectPage':
281 return 'redirectPage';
282 case 'mailNotify':
283 return 'mailNotify';
284 case 'integ':
285 case 'webHooks':
286 return 'integrations';
287 default:
288 return null;
289 }
290 }
291
292 private static function extractActionIds($detailId)
293 {
294 $ids = [];
295 $items = is_array($detailId) ? $detailId : [$detailId];
296 foreach ($items as $item) {
297 if (!is_string($item)) {
298 continue;
299 }
300 $decoded = json_decode($item);
301 if (isset($decoded->id)) {
302 $ids[] = (string) $decoded->id;
303 }
304 }
305 return $ids;
306 }
307
308 /**
309 * @deprecated misspelled duplicate of calculate(); kept because it is public API. Same
310 * divide-by-zero guard so an external caller cannot fatal either.
311 */
312 public static function calculte($firstOperand, $secondOperand, $operator)
313 {
314 return self::calculate($firstOperand, $secondOperand, $operator);
315 }
316
317 public static function filterMailContentType()
318 {
319 return 'text/html';
320 }
321
322 public static function evalMathExpression($stringWithFieldValue)
323 {
324 $mathExpr = $stringWithFieldValue;
325 if (empty($mathExpr) || !\is_scalar($mathExpr)) {
326 return $stringWithFieldValue;
327 }
328 $mathExpr = (string) $mathExpr;
329
330 // The operand/operator checks below only look at \w+ runs and operator runs, so any other
331 // character was invisible to them. A quoted date ('2020-10-10') therefore passed as a
332 // subtraction chain and its quotes later surfaced as a bogus operator token. Require the whole
333 // string to be made of things a formula can contain.
334 if (1 !== preg_match('#^[0-9.+\-*/^()\[\]{}\s]+$#', $mathExpr)) {
335 return $stringWithFieldValue;
336 }
337
338 // A bare date is not a subtraction: 2020-10-10 must stay a date, not become 2000.
339 if (1 === preg_match('/^\s*\d{4}-\d{1,2}-\d{1,2}\s*$/', $mathExpr)) {
340 return $stringWithFieldValue;
341 }
342
343 preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr);
344 if (empty($isMathExpr[0])) {
345 return $stringWithFieldValue;
346 }
347 preg_match_all('/\w+/', $mathExpr, $exprValues);
348 if (empty($exprValues[0])) {
349 return $stringWithFieldValue;
350 }
351 foreach ($exprValues[0] as $opreands) {
352 if (!is_numeric($opreands)) {
353 return $stringWithFieldValue;
354 }
355 }
356 $validOperator = ['+', '-', '*', '^', '/'];
357 foreach ($isMathExpr[0] as $value) {
358 if (!in_array(trim($value), $validOperator)) {
359 return $stringWithFieldValue;
360 }
361 }
362 $mathExpr = str_replace(' ', '', $mathExpr);
363 $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr);
364 $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr);
365 $calculated = self::infixToPostfixEvalute($mathExpr);
366 if (!is_null($calculated) && isset($calculated[0])) {
367 return (string) $calculated[0];
368 }
369
370 return (string) $stringWithFieldValue;
371 }
372
373 public static function infixToPostfixEvalute($expression)
374 {
375 $operatorStack = [];
376 $outputQueue = [];
377 $numTemp = null;
378 for ($strIndex = 0; $strIndex < strlen($expression); $strIndex++) {
379 $token = $expression[$strIndex];
380 if ('+' === $token || '-' === $token || '*' === $token || '/' === $token || '^' === $token || '(' === $token || ')' === $token) {
381 if (!is_null($numTemp)) {
382 $outputQueue[] = $numTemp;
383 $numTemp = null;
384 }
385 $stackSize = count($operatorStack);
386 if ($stackSize) {
387 $stackTop = $operatorStack[$stackSize - 1];
388 }
389 if ('(' === $token) {
390 $operatorStack[] = $token;
391 } elseif (')' === $token) {
392 // An unbalanced ')' used to read $operatorStack[-1] and warn on every iteration; treat
393 // the expression as non-arithmetic instead.
394 if (empty($operatorStack)) {
395 return null;
396 }
397 while ('(' !== $operatorStack[count($operatorStack) - 1]) {
398 $outputQueue[] = array_pop($operatorStack);
399 if (empty($operatorStack)) {
400 return null;
401 }
402 if ('(' === $operatorStack[count($operatorStack) - 1]) {
403 array_pop($operatorStack);
404 break;
405 }
406 }
407 } elseif (isset($stackTop) && self::operatorPrecedence($token) > self::operatorPrecedence($stackTop)) {
408 $operatorStack[] = $token;
409 } elseif ('^' !== $token && $stackSize) {
410 $operatorStack[$stackSize - 1] = $token;
411 $outputQueue[] = $stackTop;
412 } else {
413 $operatorStack[] = $token;
414 }
415 continue;
416 }
417 $numTemp .= $token;
418 if ($strIndex === strlen($expression) - 1 && !is_null($numTemp)) {
419 $outputQueue[] = $numTemp;
420 }
421 }
422
423 if (!is_null($operatorStack)) {
424 $outputQueue = array_merge($outputQueue, array_reverse($operatorStack));
425 }
426 $resultStack = [];
427 foreach ($outputQueue as $value) {
428 if (is_numeric($value)) {
429 $resultStack[] = $value;
430 continue;
431 }
432 // A token that is neither a number nor a real operator means the input was never an
433 // expression — e.g. a quoted date whose quotes accumulated into a token like "'2020".
434 // Bail out so evalMathExpression() returns the caller's string untouched; pushing the
435 // failure onto the stack would end up blanking that string.
436 if (!\in_array($value, ['+', '-', '*', '/', '^'], true) || count($resultStack) < 2) {
437 return null;
438 }
439 $secondOperand = array_pop($resultStack);
440 $firstOperand = array_pop($resultStack);
441 $calculated = self::calculate($firstOperand, $secondOperand, $value);
442 if (\is_null($calculated)) {
443 return null;
444 }
445 $resultStack[] = $calculated;
446 }
447 return $resultStack;
448 }
449
450 public static function operatorPrecedence($operator)
451 {
452 $precedence = [
453 '+' => 2,
454 '-' => 2,
455 '*' => 3,
456 '/' => 3,
457 '^' => 4,
458 ];
459
460 return isset($precedence[$operator]) ? $precedence[$operator] : 0;
461 }
462
463 /**
464 * Apply one arithmetic operator.
465 *
466 * @param mixed $firstOperand
467 * @param mixed $secondOperand
468 * @param string $operator
469 *
470 * @return float|int|null null on unknown operator, non-numeric operand, or division by zero
471 */
472 public static function calculate($firstOperand, $secondOperand, $operator)
473 {
474 if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) {
475 return null;
476 }
477 $firstOperand = $firstOperand + 0;
478 $secondOperand = $secondOperand + 0;
479
480 switch ($operator) {
481 case '+':
482 return $firstOperand + $secondOperand;
483 case '-':
484 return $firstOperand - $secondOperand;
485 case '*':
486 return $firstOperand * $secondOperand;
487 case '/':
488 return 0 == $secondOperand ? null : $firstOperand / $secondOperand;
489 case '^':
490 return $firstOperand ** $secondOperand;
491 }
492
493 return null;
494 }
495
496 /**
497 * Recursively sets a nested property in a given object.
498 *
499 * This function takes an object, a string path representing nested properties
500 * (e.g., "fk->valid->hide"), and a value to assign. It ensures that all
501 * intermediate properties exist as objects before setting the final value.
502 *
503 * @param object $object The main object where properties should be set.
504 * @param string $path The nested property path, with keys separated by "->".
505 * @param mixed $value The value to assign to the final property.
506 *
507 * @return void
508 */
509 public static function setNestedProperty(&$object, $path, $value)
510 {
511 $keys = explode('->', $path);
512 $key = array_shift($keys);
513
514 if (!isset($object->$key) || !is_object($object->$key)) {
515 $object->$key = new \stdClass();
516 }
517
518 if (!empty($keys)) {
519 self::setNestedProperty($object->$key, implode('->', $keys), $value);
520 } else {
521 $object->$key = $value;
522 }
523 }
524 }
525