| @@ -610,8 +610,26 @@ | ||
| 610 | 610 | |
| 611 | 611 | return apply_filters('fluentform/available_payment_statuses', $paymentStatuses); |
| 612 | 612 | } |
| 613 | 613 | |
| 614 | + public static function reversedPaymentStatuses() | |
| 615 | + { | |
| 616 | + return apply_filters('fluentform/reversed_payment_statuses', [ | |
| 617 | + 'refunded', 'partially-refunded', 'cancelled', | |
| 618 | + ]); | |
| 619 | + } | |
| 620 | + | |
| 621 | + public static function isReversedPaymentStatus($status) | |
| 622 | + { | |
| 623 | + $status = is_null($status) ? '' : (string) $status; | |
| 624 | + | |
| 625 | + if ('' === $status) { | |
| 626 | + return false; | |
| 627 | + } | |
| 628 | + | |
| 629 | + return in_array($status, static::reversedPaymentStatuses(), true); | |
| 630 | + } | |
| 631 | + | |
| 614 | 632 | public static function getFormPaymentMethods($formId) |
| 615 | 633 | { |
| 616 | 634 | $inputs = FormFieldsParser::getInputs($formId, ['element', 'settings']); |
| 617 | 635 | foreach ($inputs as $field) { |
| @@ -855,8 +873,12 @@ | ||
| 855 | 873 | } |
| 856 | 874 | |
| 857 | 875 | $form = \FluentForm\App\Models\Form::find($submission->form_id); |
| 858 | 876 | |
| 877 | + if (!apply_filters('fluentform/should_process_submission_actions', true, $submission, $form)) { | |
| 878 | + return false; | |
| 879 | + } | |
| 880 | + | |
| 859 | 881 | $formData = $submission->response; |
| 860 | 882 | if (!is_array($formData)) { |
| 861 | 883 | $formData = json_decode($formData, true); |
| 862 | 884 | } |
| @@ -1005,15 +1027,18 @@ | ||
| 1005 | 1027 | ); |
| 1006 | 1028 | |
| 1007 | 1029 | $billingInterval = $plan['billing_interval']; |
| 1008 | 1030 | $billingInterval = ArrayHelper::get(self::getBillingIntervals(), $billingInterval, $billingInterval); |
| 1031 | + $billingInterval = esc_html($billingInterval); | |
| 1032 | + $trialDays = esc_html(ArrayHelper::get($plan, 'trial_days')); | |
| 1033 | + $billTimes = esc_html(ArrayHelper::get($plan, 'bill_times')); | |
| 1009 | 1034 | $replaces = array( |
| 1010 | 1035 | '{signup_fee}' => '<span class="ff_bs ffbs_signup_fee">' . $signupFee . '</span>', |
| 1011 | 1036 | '{first_interval_total}' => '<span class="ff_bs ffbs_first_interval_total">' . $firstIntervalTotal . '</span>', |
| 1012 | 1037 | '{subscription_amount}' => '<span class="ff_bs ffbs_subscription_amount">' . $subscriptionAmount . '</span>', |
| 1013 | 1038 | '{billing_interval}' => '<span class="ff_bs ffbs_billing_interval">' . $billingInterval . '</span>', |
| 1014 | - '{trial_days}' => '<span class="ff_bs ffbs_trial_days">' . $plan['trial_days'] . '</span>', | |
| 1015 | - '{bill_times}' => '<span class="ff_bs ffbs_bill_times">' . ArrayHelper::get($plan, 'bill_times') . '</span>' | |
| 1039 | + '{trial_days}' => '<span class="ff_bs ffbs_trial_days">' . $trialDays . '</span>', | |
| 1040 | + '{bill_times}' => '<span class="ff_bs ffbs_bill_times">' . $billTimes . '</span>', | |
| 1016 | 1041 | ); |
| 1017 | 1042 | |
| 1018 | 1043 | if (ArrayHelper::get($plan, 'user_input') == 'yes') { |
| 1019 | 1044 | $cases['{subscription_amount}'] = '<span class="ff_dynamic_input_amount">' . $subscriptionAmount . '</span>'; |
| @@ -1044,9 +1069,9 @@ | ||
| 1044 | 1069 | $customText .= $cases['bill_times']; |
| 1045 | 1070 | } |
| 1046 | 1071 | if($withMarkup) { |
| 1047 | 1072 | $class = $plan['is_default'] === 'yes' ? '' : 'hidden_field'; |
| 1048 | - return '<div class="ff_summary_container ff_summary_container_' . $plan['index'] . ' ' . $class . '">' . $customText . '</div>'; | |
| 1073 | + return '<div class="ff_summary_container ff_summary_container_' . esc_attr($plan['index']) . ' ' . $class . '">' . $customText . '</div>'; | |
| 1049 | 1074 | } |
| 1050 | 1075 | return $customText; |
| 1051 | 1076 | } |
| 1052 | 1077 | |
| @@ -1092,8 +1117,36 @@ | ||
| 1092 | 1117 | } |
| 1093 | 1118 | return ''; |
| 1094 | 1119 | } |
| 1095 | 1120 | |
| 1121 | + /** | |
| 1122 | + * Stable encryption key, decoupled from WordPress salts (which some hosts and | |
| 1123 | + * security plugins rotate, silently breaking salt-encrypted payment keys). | |
| 1124 | + * | |
| 1125 | + * For stronger at-rest protection, define FLUENTFORM_ENCRYPTION_KEY in | |
| 1126 | + * wp-config.php so the key lives on the filesystem, not the database next to | |
| 1127 | + * the ciphertext. Set it BEFORE saving keys — defining it after keys are | |
| 1128 | + * stored makes existing values unreadable and requires re-entering them. | |
| 1129 | + * | |
| 1130 | + * @return string | |
| 1131 | + */ | |
| 1132 | + public static function getEncryptionKey() | |
| 1133 | + { | |
| 1134 | + if (defined('FLUENTFORM_ENCRYPTION_KEY') && FLUENTFORM_ENCRYPTION_KEY) { | |
| 1135 | + return FLUENTFORM_ENCRYPTION_KEY; | |
| 1136 | + } | |
| 1137 | + | |
| 1138 | + $key = get_option('_fluentform_encryption_key'); | |
| 1139 | + if (!$key) { | |
| 1140 | + $key = base64_encode(openssl_random_pseudo_bytes(32)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 1141 | + if (!add_option('_fluentform_encryption_key', $key, '', 'no')) { | |
| 1142 | + $key = get_option('_fluentform_encryption_key'); | |
| 1143 | + } | |
| 1144 | + } | |
| 1145 | + | |
| 1146 | + return $key; | |
| 1147 | + } | |
| 1148 | + | |
| 1096 | 1149 | public static function encryptKey($value) |
| 1097 | 1150 | { |
| 1098 | 1151 | if(!$value) { |
| 1099 | 1152 | return $value; |
| @@ -1102,24 +1155,71 @@ | ||
| 1102 | 1155 | if ( ! extension_loaded( 'openssl' ) ) { |
| 1103 | 1156 | return $value; |
| 1104 | 1157 | } |
| 1105 | 1158 | |
| 1106 | - $salt = (defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure'; | |
| 1107 | - $key = ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure'; | |
| 1108 | - | |
| 1159 | + $key = self::getEncryptionKey(); | |
| 1109 | 1160 | $method = 'aes-256-ctr'; |
| 1110 | 1161 | $ivlen = openssl_cipher_iv_length( $method ); |
| 1111 | 1162 | $iv = openssl_random_pseudo_bytes( $ivlen ); |
| 1112 | 1163 | |
| 1113 | - $raw_value = openssl_encrypt( $value . $salt, $method, $key, 0, $iv ); | |
| 1114 | - if ( ! $raw_value ) { | |
| 1164 | + $ciphertext = openssl_encrypt( $value, $method, $key, OPENSSL_RAW_DATA, $iv ); | |
| 1165 | + if ( $ciphertext === false ) { | |
| 1115 | 1166 | return false; |
| 1116 | 1167 | } |
| 1117 | 1168 | |
| 1118 | - return base64_encode( $iv . $raw_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 1169 | + $hmac = hash_hmac( 'sha256', $iv . $ciphertext, $key, true ); | |
| 1170 | + | |
| 1171 | + return 'v2:' . base64_encode( $iv . $hmac . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 1119 | 1172 | } |
| 1120 | 1173 | |
| 1121 | 1174 | public static function decryptKey( $raw_value ) { |
| 1175 | + | |
| 1176 | + if(!$raw_value) { | |
| 1177 | + return $raw_value; | |
| 1178 | + } | |
| 1179 | + | |
| 1180 | + if ( strpos( $raw_value, 'v2:' ) !== 0 ) { | |
| 1181 | + return self::legacyDecryptKey( $raw_value ); | |
| 1182 | + } | |
| 1183 | + | |
| 1184 | + // A v2 blob is unrecoverable without openssl, so fail loud instead of | |
| 1185 | + // handing the ciphertext back as if it were the key. | |
| 1186 | + if ( ! extension_loaded( 'openssl' ) ) { | |
| 1187 | + return false; | |
| 1188 | + } | |
| 1189 | + | |
| 1190 | + $key = self::getEncryptionKey(); | |
| 1191 | + $decoded = base64_decode( substr( $raw_value, 3 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 1192 | + $method = 'aes-256-ctr'; | |
| 1193 | + $ivlen = openssl_cipher_iv_length( $method ); | |
| 1194 | + $sha2len = 32; | |
| 1195 | + | |
| 1196 | + if ( $decoded === false || strlen( $decoded ) < $ivlen + $sha2len ) { | |
| 1197 | + return false; | |
| 1198 | + } | |
| 1199 | + | |
| 1200 | + $iv = substr( $decoded, 0, $ivlen ); | |
| 1201 | + $hmac = substr( $decoded, $ivlen, $sha2len ); | |
| 1202 | + $ciphertext = substr( $decoded, $ivlen + $sha2len ); | |
| 1203 | + | |
| 1204 | + // Encrypt-then-MAC: verify integrity (constant time) before decrypting. | |
| 1205 | + $calcmac = hash_hmac( 'sha256', $iv . $ciphertext, $key, true ); | |
| 1206 | + if ( ! hash_equals( $hmac, $calcmac ) ) { | |
| 1207 | + return false; | |
| 1208 | + } | |
| 1209 | + | |
| 1210 | + $value = openssl_decrypt( $ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv ); | |
| 1211 | + | |
| 1212 | + return $value !== false ? $value : false; | |
| 1213 | + } | |
| 1214 | + | |
| 1215 | + /** | |
| 1216 | + * Reads keys encrypted before v2, i.e. tied to LOGGED_IN_KEY / LOGGED_IN_SALT. | |
| 1217 | + * | |
| 1218 | + * @param string $raw_value | |
| 1219 | + * @return string|bool | |
| 1220 | + */ | |
| 1221 | + public static function legacyDecryptKey( $raw_value ) { | |
| 1122 | 1222 | |
| 1123 | 1223 | if(!$raw_value) { |
| 1124 | 1224 | return $raw_value; |
| 1125 | 1225 | } |