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

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

550 lines 19.2 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 ('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 }
29 if (isset($fieldDetail->mul)) {
30 $fieldData[$fieldKey] =
31 array_merge(
32 $fieldData[$fieldKey],
33 [
34 'mul' => $fieldDetail->mul,
35 ]
36 );
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 }
43 }
44 return $fieldData;
45 }
46
47 public static function smartFldMargeFormFld($logics, $fieldData)
48 {
49 $fieldKeys = SmartTags::smartTagFieldKeys();
50
51 foreach ($logics as $logic) {
52 $removeSpecialStr = ['${', '}', '()'];
53 if (isset($logic->field) && in_array(str_replace($removeSpecialStr, '', $logic->field), $fieldKeys)) {
54 $field = str_replace($removeSpecialStr, '', $logic->field);
55
56 $fldKey = '${' . $field . '}';
57 $customValue = isset($logic->smartKey) ? $logic->smartKey : '';
58 $val = SmartTags::getSmartTagValue($field, false, $customValue);
59
60 $fieldData[$fldKey] = [
61 'key' => $fldKey,
62 'value' => empty($val) ? '' : $val,
63 'type' => is_string($val) ? 'text' : 'array',
64 ];
65 }
66 }
67 return $fieldData;
68 }
69
70 public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null, $stripShortcodesFromValues = false)
71 {
72 $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID, $stripShortcodesFromValues);
73 if ($evalMathExpr) {
74 return self::evalMathExpression($stringToReplaceField);
75 }
76 return $stringToReplaceField;
77 }
78
79 public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0, $workFlowRun = null)
80 {
81 $messageId = 0;
82 $returnableData = null;
83 $afterSubmit = null;
84 $msgDuration = null;
85 $integrationHandler = new IntegrationHandler($formId);
86 $conditionalActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun);
87 switch ($confirmationType) {
88 case 'successMsg':
89 // Standalone confirmation: show the first ENABLED success message without requiring a workflow.
90 $successMessageHandler = new SuccessMessageHandler($formId);
91 $successMessage = $successMessageHandler->getAllMessage();
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 }
118 }
119 break;
120 case 'redirectPage':
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;
140 }
141 }
142 break;
143 case 'webHooks':
144 // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases).
145 break;
146 default:
147 break;
148 }
149
150 return [
151 'msg_id' => $messageId,
152 'confirmation' => $returnableData,
153 'afterSubmit' => $afterSubmit,
154 'msg_duration' => $msgDuration,
155 ];
156 }
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 */
335 public static function calculte($firstOperand, $secondOperand, $operator)
336 {
337 return self::calculate($firstOperand, $secondOperand, $operator);
338 }
339
340 public static function filterMailContentType()
341 {
342 return 'text/html';
343 }
344
345 public static function evalMathExpression($stringWithFieldValue)
346 {
347 $mathExpr = $stringWithFieldValue;
348 if (empty($mathExpr) || !\is_scalar($mathExpr)) {
349 return $stringWithFieldValue;
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
366 preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr);
367 if (empty($isMathExpr[0])) {
368 return $stringWithFieldValue;
369 }
370 preg_match_all('/\w+/', $mathExpr, $exprValues);
371 if (empty($exprValues[0])) {
372 return $stringWithFieldValue;
373 }
374 foreach ($exprValues[0] as $opreands) {
375 if (!is_numeric($opreands)) {
376 return $stringWithFieldValue;
377 }
378 }
379 $validOperator = ['+', '-', '*', '^', '/'];
380 foreach ($isMathExpr[0] as $value) {
381 if (!in_array(trim($value), $validOperator)) {
382 return $stringWithFieldValue;
383 }
384 }
385 $mathExpr = str_replace(' ', '', $mathExpr);
386 $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr);
387 $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr);
388 $calculated = self::infixToPostfixEvalute($mathExpr);
389 if (!is_null($calculated) && isset($calculated[0])) {
390 return (string) $calculated[0];
391 }
392
393 return (string) $stringWithFieldValue;
394 }
395
396 public static function infixToPostfixEvalute($expression)
397 {
398 $operatorStack = [];
399 $outputQueue = [];
400 $numTemp = null;
401 for ($strIndex = 0; $strIndex < strlen($expression); $strIndex++) {
402 $token = $expression[$strIndex];
403 if ('+' === $token || '-' === $token || '*' === $token || '/' === $token || '^' === $token || '(' === $token || ')' === $token) {
404 if (!is_null($numTemp)) {
405 $outputQueue[] = $numTemp;
406 $numTemp = null;
407 }
408 $stackSize = count($operatorStack);
409 if ($stackSize) {
410 $stackTop = $operatorStack[$stackSize - 1];
411 }
412 if ('(' === $token) {
413 $operatorStack[] = $token;
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 }
420 while ('(' !== $operatorStack[count($operatorStack) - 1]) {
421 $outputQueue[] = array_pop($operatorStack);
422 if (empty($operatorStack)) {
423 return null;
424 }
425 if ('(' === $operatorStack[count($operatorStack) - 1]) {
426 array_pop($operatorStack);
427 break;
428 }
429 }
430 } elseif (isset($stackTop) && self::operatorPrecedence($token) > self::operatorPrecedence($stackTop)) {
431 $operatorStack[] = $token;
432 } elseif ('^' !== $token && $stackSize) {
433 $operatorStack[$stackSize - 1] = $token;
434 $outputQueue[] = $stackTop;
435 } else {
436 $operatorStack[] = $token;
437 }
438 continue;
439 }
440 $numTemp .= $token;
441 if ($strIndex === strlen($expression) - 1 && !is_null($numTemp)) {
442 $outputQueue[] = $numTemp;
443 }
444 }
445
446 if (!is_null($operatorStack)) {
447 $outputQueue = array_merge($outputQueue, array_reverse($operatorStack));
448 }
449 $resultStack = [];
450 foreach ($outputQueue as $value) {
451 if (is_numeric($value)) {
452 $resultStack[] = $value;
453 continue;
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 }
462 $secondOperand = array_pop($resultStack);
463 $firstOperand = array_pop($resultStack);
464 $calculated = self::calculate($firstOperand, $secondOperand, $value);
465 if (\is_null($calculated)) {
466 return null;
467 }
468 $resultStack[] = $calculated;
469 }
470 return $resultStack;
471 }
472
473 public static function operatorPrecedence($operator)
474 {
475 $precedence = [
476 '+' => 2,
477 '-' => 2,
478 '*' => 3,
479 '/' => 3,
480 '^' => 4,
481 ];
482
483 return isset($precedence[$operator]) ? $precedence[$operator] : 0;
484 }
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 */
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;
502
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 }
548 }
549 }
550