| @@ -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) |
| @@ -13,11 +15,12 @@ | ||
| 13 | 15 | { |
| 14 | 16 | $fieldData = []; |
| 15 | 17 | |
| 16 | 18 | foreach ($fields as $fieldKey => $fieldDetail) { |
| 19 | + $value = isset($fieldDetail->val) ? $fieldDetail->val : (isset($fieldDetail->defaultValue) ? $fieldDetail->defaultValue : ''); | |
| 17 | 20 | $fieldData[$fieldKey] = [ |
| 18 | 21 | 'key' => $fieldKey, |
| 19 | - 'value' => empty($fieldDetail->val) ? '' : $fieldDetail->val, | |
| 22 | + 'value' => $value, | |
| 20 | 23 | 'type' => $fieldDetail->typ, |
| 21 | 24 | ]; |
| 22 | 25 | if (isset($fieldDetail->mul)) { |
| 23 | 26 | $fieldData[$fieldKey] = |
| @@ -54,11 +57,11 @@ | ||
| 54 | 57 | } |
| 55 | 58 | return $fieldData; |
| 56 | 59 | } |
| 57 | 60 | |
| 58 | - public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true) | |
| 61 | + public static function replaceFieldWithValue($stringToReplaceField, $fieldValues, $evalMathExpr = true, $formID = null, $stripShortcodesFromValues = false) | |
| 59 | 62 | { |
| 60 | - $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues); | |
| 63 | + $stringToReplaceField = FieldValueHandler::replaceFieldWithValue($stringToReplaceField, $fieldValues, $formID, $stripShortcodesFromValues); | |
| 61 | 64 | if ($evalMathExpr) { |
| 62 | 65 | return self::evalMathExpression($stringToReplaceField); |
| 63 | 66 | } |
| 64 | 67 | return $stringToReplaceField; |
| @@ -63,42 +66,74 @@ | ||
| 63 | 66 | } |
| 64 | 67 | return $stringToReplaceField; |
| 65 | 68 | } |
| 66 | 69 | |
| 67 | - public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0) | |
| 70 | + public static function setDefaultSubmitConfirmation($confirmationType, $fieldValue, $formId, $logID = 0, $workFlowRun = null) | |
| 68 | 71 | { |
| 69 | 72 | $messageId = 0; |
| 70 | 73 | $returnableData = null; |
| 71 | - return [ | |
| 72 | - 'msg_id' => $messageId, | |
| 73 | - 'confirmation' => $returnableData, | |
| 74 | - ]; | |
| 74 | + $afterSubmit = null; | |
| 75 | + $msgDuration = null; | |
| 75 | 76 | $integrationHandler = new IntegrationHandler($formId); |
| 77 | + $conditionalActionIds = self::getDefaultSubmitExcludedActionIds($formId, $workFlowRun); | |
| 76 | 78 | switch ($confirmationType) { |
| 77 | 79 | case 'successMsg': |
| 78 | - $successMessageHandler | |
| 79 | - = new SuccessMessageHandler($formId); | |
| 80 | + // Standalone confirmation: show the first ENABLED success message without requiring a workflow. | |
| 81 | + $successMessageHandler = new SuccessMessageHandler($formId); | |
| 80 | 82 | $successMessage = $successMessageHandler->getAllMessage(); |
| 81 | - if (!is_wp_error($successMessage) && !empty($successMessage) && count($successMessage) > 0) { | |
| 82 | - $returnableData = self::replaceFieldWithValue($successMessage[0]->message_content, $fieldValue); | |
| 83 | - $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 | + } | |
| 84 | 109 | } |
| 85 | 110 | break; |
| 86 | 111 | case 'redirectPage': |
| 87 | - $redirectPage = $integrationHandler->getAllIntegration('form', 'redirectPage'); | |
| 88 | - if (!is_wp_error($redirectPage) && !empty($redirectPage) && count($redirectPage) > 0) { | |
| 89 | - $url = json_decode($redirectPage[0]->integration_details)->url; | |
| 90 | - if (!empty($url)) { | |
| 91 | - $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; | |
| 92 | 131 | } |
| 93 | - $returnableData = empty($url) ? '' : esc_url_raw($url); | |
| 94 | 132 | } |
| 95 | 133 | break; |
| 96 | 134 | case 'webHooks': |
| 97 | - $webHooks = $integrationHandler->getAllIntegration('form', 'webHooks'); | |
| 98 | - if (!is_wp_error($webHooks) && !empty($webHooks) && 1 === count($webHooks)) { | |
| 99 | - $returnableData = ["{\"id\":{$webHooks[0]->id}}"]; | |
| 100 | - } | |
| 135 | + // Default redirect/webhook remain workflow-driven for now (revisited in the Redirect/Integration phases). | |
| 101 | 136 | break; |
| 102 | 137 | default: |
| 103 | 138 | break; |
| 104 | 139 | } |
| @@ -105,25 +140,193 @@ | ||
| 105 | 140 | |
| 106 | 141 | return [ |
| 107 | 142 | 'msg_id' => $messageId, |
| 108 | 143 | 'confirmation' => $returnableData, |
| 144 | + 'afterSubmit' => $afterSubmit, | |
| 145 | + 'msg_duration' => $msgDuration, | |
| 109 | 146 | ]; |
| 110 | 147 | } |
| 111 | 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 | + */ | |
| 112 | 326 | public static function calculte($firstOperand, $secondOperand, $operator) |
| 113 | 327 | { |
| 114 | - switch ($operator) { | |
| 115 | - case '+': | |
| 116 | - return $firstOperand + $secondOperand; | |
| 117 | - case '-': | |
| 118 | - return $firstOperand - $secondOperand; | |
| 119 | - case '*': | |
| 120 | - return $firstOperand * $secondOperand; | |
| 121 | - case '/': | |
| 122 | - return $firstOperand / $secondOperand; | |
| 123 | - case '^': | |
| 124 | - return $firstOperand ** $secondOperand; | |
| 125 | - } | |
| 328 | + return self::calculate($firstOperand, $secondOperand, $operator); | |
| 126 | 329 | } |
| 127 | 330 | |
| 128 | 331 | public static function filterMailContentType() |
| 129 | 332 | { |
| @@ -132,11 +335,26 @@ | ||
| 132 | 335 | |
| 133 | 336 | public static function evalMathExpression($stringWithFieldValue) |
| 134 | 337 | { |
| 135 | 338 | $mathExpr = $stringWithFieldValue; |
| 136 | - if (empty($mathExpr)) { | |
| 339 | + if (empty($mathExpr) || !\is_scalar($mathExpr)) { | |
| 137 | 340 | return $stringWithFieldValue; |
| 138 | 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 | + | |
| 139 | 357 | preg_match_all('/[\+\-\*\/\s]+/', $mathExpr, $isMathExpr); |
| 140 | 358 | if (empty($isMathExpr[0])) { |
| 141 | 359 | return $stringWithFieldValue; |
| 142 | 360 | } |
| @@ -158,9 +376,9 @@ | ||
| 158 | 376 | $mathExpr = str_replace(' ', '', $mathExpr); |
| 159 | 377 | $mathExpr = preg_replace('/\{|\[|\(/', '(', $mathExpr); |
| 160 | 378 | $mathExpr = preg_replace('/\}|\]/', ')', $mathExpr); |
| 161 | 379 | $calculated = self::infixToPostfixEvalute($mathExpr); |
| 162 | - if (!is_null($calculated)) { | |
| 380 | + if (!is_null($calculated) && isset($calculated[0])) { | |
| 163 | 381 | return (string) $calculated[0]; |
| 164 | 382 | } |
| 165 | 383 | |
| 166 | 384 | return (string) $stringWithFieldValue; |
| @@ -184,10 +402,18 @@ | ||
| 184 | 402 | } |
| 185 | 403 | if ('(' === $token) { |
| 186 | 404 | $operatorStack[] = $token; |
| 187 | 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 | + } | |
| 188 | 411 | while ('(' !== $operatorStack[count($operatorStack) - 1]) { |
| 189 | 412 | $outputQueue[] = array_pop($operatorStack); |
| 413 | + if (empty($operatorStack)) { | |
| 414 | + return null; | |
| 415 | + } | |
| 190 | 416 | if ('(' === $operatorStack[count($operatorStack) - 1]) { |
| 191 | 417 | array_pop($operatorStack); |
| 192 | 418 | break; |
| 193 | 419 | } |
| @@ -216,11 +442,22 @@ | ||
| 216 | 442 | if (is_numeric($value)) { |
| 217 | 443 | $resultStack[] = $value; |
| 218 | 444 | continue; |
| 219 | 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 | + } | |
| 220 | 453 | $secondOperand = array_pop($resultStack); |
| 221 | 454 | $firstOperand = array_pop($resultStack); |
| 222 | - $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; | |
| 223 | 460 | } |
| 224 | 461 | return $resultStack; |
| 225 | 462 | } |
| 226 | 463 | |
| @@ -236,17 +473,68 @@ | ||
| 236 | 473 | |
| 237 | 474 | return isset($precedence[$operator]) ? $precedence[$operator] : 0; |
| 238 | 475 | } |
| 239 | 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 | + */ | |
| 240 | 486 | public static function calculate($firstOperand, $secondOperand, $operator) |
| 241 | 487 | { |
| 242 | - $calculated = [ | |
| 243 | - '+' => $firstOperand + $secondOperand, | |
| 244 | - '-' => $firstOperand - $secondOperand, | |
| 245 | - '*' => $firstOperand * $secondOperand, | |
| 246 | - '/' => $firstOperand / $secondOperand, | |
| 247 | - '^' => $firstOperand ** $secondOperand, | |
| 248 | - ]; | |
| 488 | + if (!is_numeric($firstOperand) || !is_numeric($secondOperand)) { | |
| 489 | + return null; | |
| 490 | + } | |
| 491 | + $firstOperand = $firstOperand + 0; | |
| 492 | + $secondOperand = $secondOperand + 0; | |
| 249 | 493 | |
| 250 | - 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; | |
| 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 | + } | |
| 251 | 539 | } |
| 252 | 540 | } |