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
bit-form / includes / Core / WorkFlow / Helper.php

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

541 lines 18.6 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 // 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 }
109 }
110 break;
111 case 'redirectPage':
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;
131 }
132 }
133 break;
134 case 'webHooks':
135 // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases).
136 break;
137 default:
138 break;
139 }
140
141 return [
142 'msg_id' => $messageId,
143 'confirmation' => $returnableData,
144 'afterSubmit' => $afterSubmit,
145 'msg_duration' => $msgDuration,
146 ];
147 }
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 */
326 public static function calculte($firstOperand, $secondOperand, $operator)
327 {
328 return self::calculate($firstOperand, $secondOperand, $operator);
329 }
330
331 public static function filterMailContentType()
332 {
333 return 'text/html';
334 }
335
336 public static function evalMathExpression($stringWithFieldValue)
337 {
338 $mathExpr = $stringWithFieldValue;
339 if (empty($mathExpr) || !\is_scalar($mathExpr)) {
340 return $stringWithFieldValue;
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
357 preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr);
358 if (empty($isMathExpr[0])) {
359 return $stringWithFieldValue;
360 }
361 preg_match_all('/\w+/', $mathExpr, $exprValues);
362 if (empty($exprValues[0])) {
363 return $stringWithFieldValue;
364 }
365 foreach ($exprValues[0] as $opreands) {
366 if (!is_numeric($opreands)) {
367 return $stringWithFieldValue;
368 }
369 }
370 $validOperator = ['+', '-', '*', '^', '/'];
371 foreach ($isMathExpr[0] as $value) {
372 if (!in_array(trim($value), $validOperator)) {
373 return $stringWithFieldValue;
374 }
375 }
376 $mathExpr = str_replace(' ', '', $mathExpr);
377 $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr);
378 $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr);
379 $calculated = self::infixToPostfixEvalute($mathExpr);
380 if (!is_null($calculated) && isset($calculated[0])) {
381 return (string) $calculated[0];
382 }
383
384 return (string) $stringWithFieldValue;
385 }
386
387 public static function infixToPostfixEvalute($expression)
388 {
389 $operatorStack = [];
390 $outputQueue = [];
391 $numTemp = null;
392 for ($strIndex = 0; $strIndex < strlen($expression); $strIndex++) {
393 $token = $expression[$strIndex];
394 if ('+' === $token || '-' === $token || '*' === $token || '/' === $token || '^' === $token || '(' === $token || ')' === $token) {
395 if (!is_null($numTemp)) {
396 $outputQueue[] = $numTemp;
397 $numTemp = null;
398 }
399 $stackSize = count($operatorStack);
400 if ($stackSize) {
401 $stackTop = $operatorStack[$stackSize - 1];
402 }
403 if ('(' === $token) {
404 $operatorStack[] = $token;
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 }
411 while ('(' !== $operatorStack[count($operatorStack) - 1]) {
412 $outputQueue[] = array_pop($operatorStack);
413 if (empty($operatorStack)) {
414 return null;
415 }
416 if ('(' === $operatorStack[count($operatorStack) - 1]) {
417 array_pop($operatorStack);
418 break;
419 }
420 }
421 } elseif (isset($stackTop) && self::operatorPrecedence($token) > self::operatorPrecedence($stackTop)) {
422 $operatorStack[] = $token;
423 } elseif ('^' !== $token && $stackSize) {
424 $operatorStack[$stackSize - 1] = $token;
425 $outputQueue[] = $stackTop;
426 } else {
427 $operatorStack[] = $token;
428 }
429 continue;
430 }
431 $numTemp .= $token;
432 if ($strIndex === strlen($expression) - 1 && !is_null($numTemp)) {
433 $outputQueue[] = $numTemp;
434 }
435 }
436
437 if (!is_null($operatorStack)) {
438 $outputQueue = array_merge($outputQueue, array_reverse($operatorStack));
439 }
440 $resultStack = [];
441 foreach ($outputQueue as $value) {
442 if (is_numeric($value)) {
443 $resultStack[] = $value;
444 continue;
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 }
453 $secondOperand = array_pop($resultStack);
454 $firstOperand = array_pop($resultStack);
455 $calculated = self::calculate($firstOperand, $secondOperand, $value);
456 if (\is_null($calculated)) {
457 return null;
458 }
459 $resultStack[] = $calculated;
460 }
461 return $resultStack;
462 }
463
464 public static function operatorPrecedence($operator)
465 {
466 $precedence = [
467 '+' => 2,
468 '-' => 2,
469 '*' => 3,
470 '/' => 3,
471 '^' => 4,
472 ];
473
474 return isset($precedence[$operator]) ? $precedence[$operator] : 0;
475 }
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 */
486 public static function calculate($firstOperand, $secondOperand, $operator)
487 {
488 if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) {
489 return null;
490 }
491 $firstOperand = $firstOperand + 0;
492 $secondOperand = $secondOperand + 0;
493
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;
510 }
511
512 /**
513 * Recursively sets a nested property in a given object.
514 *
515 * This function takes an object, a string path representing nested properties
516 * (e.g., "fk->valid->hide"), and a value to assign. It ensures that all
517 * intermediate properties exist as objects before setting the final value.
518 *
519 * @param object $object The main object where properties should be set.
520 * @param string $path The nested property path, with keys separated by "->".
521 * @param mixed $value The value to assign to the final property.
522 *
523 * @return void
524 */
525 public static function setNestedProperty(&$object, $path, $value)
526 {
527 $keys = explode('->', $path);
528 $key = array_shift($keys);
529
530 if (!isset($object->$key) || !is_object($object->$key)) {
531 $object->$key = new \stdClass();
532 }
533
534 if (!empty($keys)) {
535 self::setNestedProperty($object->$key, implode('->', $keys), $value);
536 } else {
537 $object->$key = $value;
538 }
539 }
540 }
541