PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
← All changes | app/Modules/Payments/PaymentHelper.php +109 -9 6.2.56.2.14 View file →
@@ -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 }