PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 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 All 197 releases
← All changes | app/Modules/Payments/PaymentHelper.php +146 -20 6.2.2 → 6.2.15 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) {
@@ -799,23 +817,47 @@
799 817
800 818 $stripe = ArrayHelper::get($methods, 'stripe');
801 819 $stripeInlineStyles = ArrayHelper::get(Helper::getFormMeta($formId, '_ff_form_styles', []), 'stripe_inline_element_style', false);
802 820 if ($stripe) {
803 - return apply_filters(
804 - 'fluentform/stripe_inline_config',
805 - [
806 - 'is_inline' => ArrayHelper::get($stripe, 'settings.embedded_checkout.value') == 'yes',
807 - 'inline_styles' => $stripeInlineStyles,
808 - 'verifyZip' => ArrayHelper::get($methods['stripe'], 'settings.verify_zip_code.value') === 'yes',
809 - 'disable_link' => false
810 - ],
811 - $formId
812 - );
821 + $config = [
822 + 'is_inline' => ArrayHelper::get($stripe, 'settings.embedded_checkout.value') == 'yes',
823 + 'inline_styles' => $stripeInlineStyles,
824 + 'verifyZip' => ArrayHelper::get($methods['stripe'], 'settings.verify_zip_code.value') === 'yes',
825 + 'disable_link' => false,
826 + 'enable_payment_element' => static::isStripePaymentElement($stripe),
827 + 'is_zero_decimal' => static::isZeroDecimal(static::getFormCurrency($formId)),
828 + ];
829 +
830 + // Classic and conversational forms both create the Payment Element with these options
831 + if ($config['enable_payment_element']) {
832 + $config['payment_element_options'] = apply_filters(
833 + 'fluentform/stripe_payment_element_options',
834 + [
835 + 'layout' => 'tabs',
836 + 'wallets' => ['applePay' => 'auto', 'googlePay' => 'auto', 'link' => 'never'],
837 + 'fields' => ['billingDetails' => ['address' => $config['verifyZip'] ? 'auto' : 'if_required']],
838 + 'appearance' => [],
839 + ],
840 + $formId
841 + );
842 + }
843 +
844 + return apply_filters('fluentform/stripe_inline_config', $config, $formId);
813 845 }
814 846
815 847 return [];
816 848 }
817 849
850 + /**
851 + * Stripe's Payment Element (card plus Apple Pay / Google Pay) replaces the Card Element on the embedded field,
852 + * so it only applies with Embedded Checkout on.
853 + */
854 + public static function isStripePaymentElement($stripeMethod)
855 + {
856 + return ArrayHelper::get($stripeMethod, 'settings.embedded_checkout.value') === 'yes'
857 + && ArrayHelper::get($stripeMethod, 'settings.enable_payment_element.value') === 'yes';
858 + }
859 +
818 860 public static function log($data, $submission = false, $forceInsert = false)
819 861 {
820 862 if (!$forceInsert) {
821 863 static $paymentSettings;
@@ -855,8 +897,12 @@
855 897 }
856 898
857 899 $form = \FluentForm\App\Models\Form::find($submission->form_id);
858 900
901 + if (!apply_filters('fluentform/should_process_submission_actions', true, $submission, $form)) {
902 + return false;
903 + }
904 +
859 905 $formData = $submission->response;
860 906 if (!is_array($formData)) {
861 907 $formData = json_decode($formData, true);
862 908 }
@@ -1005,15 +1051,18 @@
1005 1051 );
1006 1052
1007 1053 $billingInterval = $plan['billing_interval'];
1008 1054 $billingInterval = ArrayHelper::get(self::getBillingIntervals(), $billingInterval, $billingInterval);
1055 + $billingInterval = esc_html($billingInterval);
1056 + $trialDays = esc_html(ArrayHelper::get($plan, 'trial_days'));
1057 + $billTimes = esc_html(ArrayHelper::get($plan, 'bill_times'));
1009 1058 $replaces = array(
1010 1059 '{signup_fee}' => '<span class="ff_bs ffbs_signup_fee">' . $signupFee . '</span>',
1011 1060 '{first_interval_total}' => '<span class="ff_bs ffbs_first_interval_total">' . $firstIntervalTotal . '</span>',
1012 1061 '{subscription_amount}' => '<span class="ff_bs ffbs_subscription_amount">' . $subscriptionAmount . '</span>',
1013 1062 '{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>'
1063 + '{trial_days}' => '<span class="ff_bs ffbs_trial_days">' . $trialDays . '</span>',
1064 + '{bill_times}' => '<span class="ff_bs ffbs_bill_times">' . $billTimes . '</span>',
1016 1065 );
1017 1066
1018 1067 if (ArrayHelper::get($plan, 'user_input') == 'yes') {
1019 1068 $cases['{subscription_amount}'] = '<span class="ff_dynamic_input_amount">' . $subscriptionAmount . '</span>';
@@ -1023,9 +1072,11 @@
1023 1072 $cases[$textKey] = str_replace(array_keys($replaces), array_values($replaces), $text);
1024 1073 }
1025 1074
1026 1075 $customText = '';
1027 - if ($hasSignupFee) {
1076 + if ($hasSignupFee && isset($plan['bill_times']) && $plan['bill_times'] == 1) {
1077 + $customText = $cases['onetime_only'];
1078 + } else if ($hasSignupFee) {
1028 1079 $customText = $cases['has_signup_fee'];
1029 1080 } else if ($hasTrial) {
1030 1081 if (ArrayHelper::get($plan, 'bill_times') == 1) {
1031 1082 $customText = $cases['single_trial'];
@@ -1042,9 +1093,9 @@
1042 1093 $customText .= $cases['bill_times'];
1043 1094 }
1044 1095 if($withMarkup) {
1045 1096 $class = $plan['is_default'] === 'yes' ? '' : 'hidden_field';
1046 - return '<div class="ff_summary_container ff_summary_container_' . $plan['index'] . ' ' . $class . '">' . $customText . '</div>';
1097 + return '<div class="ff_summary_container ff_summary_container_' . esc_attr($plan['index']) . ' ' . $class . '">' . $customText . '</div>';
1047 1098 }
1048 1099 return $customText;
1049 1100 }
1050 1101
@@ -1090,8 +1141,36 @@
1090 1141 }
1091 1142 return '';
1092 1143 }
1093 1144
1145 + /**
1146 + * Stable encryption key, decoupled from WordPress salts (which some hosts and
1147 + * security plugins rotate, silently breaking salt-encrypted payment keys).
1148 + *
1149 + * For stronger at-rest protection, define FLUENTFORM_ENCRYPTION_KEY in
1150 + * wp-config.php so the key lives on the filesystem, not the database next to
1151 + * the ciphertext. Set it BEFORE saving keys — defining it after keys are
1152 + * stored makes existing values unreadable and requires re-entering them.
1153 + *
1154 + * @return string
1155 + */
1156 + public static function getEncryptionKey()
1157 + {
1158 + if (defined('FLUENTFORM_ENCRYPTION_KEY') && FLUENTFORM_ENCRYPTION_KEY) {
1159 + return FLUENTFORM_ENCRYPTION_KEY;
1160 + }
1161 +
1162 + $key = get_option('_fluentform_encryption_key');
1163 + if (!$key) {
1164 + $key = base64_encode(openssl_random_pseudo_bytes(32)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1165 + if (!add_option('_fluentform_encryption_key', $key, '', 'no')) {
1166 + $key = get_option('_fluentform_encryption_key');
1167 + }
1168 + }
1169 +
1170 + return $key;
1171 + }
1172 +
1094 1173 public static function encryptKey($value)
1095 1174 {
1096 1175 if(!$value) {
1097 1176 return $value;
@@ -1100,24 +1179,71 @@
1100 1179 if ( ! extension_loaded( 'openssl' ) ) {
1101 1180 return $value;
1102 1181 }
1103 1182
1104 - $salt = (defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1105 - $key = ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
1106 -
1183 + $key = self::getEncryptionKey();
1107 1184 $method = 'aes-256-ctr';
1108 1185 $ivlen = openssl_cipher_iv_length( $method );
1109 1186 $iv = openssl_random_pseudo_bytes( $ivlen );
1110 1187
1111 - $raw_value = openssl_encrypt( $value . $salt, $method, $key, 0, $iv );
1112 - if ( ! $raw_value ) {
1188 + $ciphertext = openssl_encrypt( $value, $method, $key, OPENSSL_RAW_DATA, $iv );
1189 + if ( $ciphertext === false ) {
1113 1190 return false;
1114 1191 }
1115 1192
1116 - return base64_encode( $iv . $raw_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1193 + $hmac = hash_hmac( 'sha256', $iv . $ciphertext, $key, true );
1194 +
1195 + return 'v2:' . base64_encode( $iv . $hmac . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1117 1196 }
1118 1197
1119 1198 public static function decryptKey( $raw_value ) {
1199 +
1200 + if(!$raw_value) {
1201 + return $raw_value;
1202 + }
1203 +
1204 + if ( strpos( $raw_value, 'v2:' ) !== 0 ) {
1205 + return self::legacyDecryptKey( $raw_value );
1206 + }
1207 +
1208 + // A v2 blob is unrecoverable without openssl, so fail loud instead of
1209 + // handing the ciphertext back as if it were the key.
1210 + if ( ! extension_loaded( 'openssl' ) ) {
1211 + return false;
1212 + }
1213 +
1214 + $key = self::getEncryptionKey();
1215 + $decoded = base64_decode( substr( $raw_value, 3 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1216 + $method = 'aes-256-ctr';
1217 + $ivlen = openssl_cipher_iv_length( $method );
1218 + $sha2len = 32;
1219 +
1220 + if ( $decoded === false || strlen( $decoded ) < $ivlen + $sha2len ) {
1221 + return false;
1222 + }
1223 +
1224 + $iv = substr( $decoded, 0, $ivlen );
1225 + $hmac = substr( $decoded, $ivlen, $sha2len );
1226 + $ciphertext = substr( $decoded, $ivlen + $sha2len );
1227 +
1228 + // Encrypt-then-MAC: verify integrity (constant time) before decrypting.
1229 + $calcmac = hash_hmac( 'sha256', $iv . $ciphertext, $key, true );
1230 + if ( ! hash_equals( $hmac, $calcmac ) ) {
1231 + return false;
1232 + }
1233 +
1234 + $value = openssl_decrypt( $ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv );
1235 +
1236 + return $value !== false ? $value : false;
1237 + }
1238 +
1239 + /**
1240 + * Reads keys encrypted before v2, i.e. tied to LOGGED_IN_KEY / LOGGED_IN_SALT.
1241 + *
1242 + * @param string $raw_value
1243 + * @return string|bool
1244 + */
1245 + public static function legacyDecryptKey( $raw_value ) {
1120 1246
1121 1247 if(!$raw_value) {
1122 1248 return $raw_value;
1123 1249 }