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 +143 -19 6.2.6 → 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>';
@@ -1044,9 +1093,9 @@
1044 1093 $customText .= $cases['bill_times'];
1045 1094 }
1046 1095 if($withMarkup) {
1047 1096 $class = $plan['is_default'] === 'yes' ? '' : 'hidden_field';
1048 - 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>';
1049 1098 }
1050 1099 return $customText;
1051 1100 }
1052 1101
@@ -1092,8 +1141,36 @@
1092 1141 }
1093 1142 return '';
1094 1143 }
1095 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 +
1096 1173 public static function encryptKey($value)
1097 1174 {
1098 1175 if(!$value) {
1099 1176 return $value;
@@ -1102,24 +1179,71 @@
1102 1179 if ( ! extension_loaded( 'openssl' ) ) {
1103 1180 return $value;
1104 1181 }
1105 1182
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 -
1183 + $key = self::getEncryptionKey();
1109 1184 $method = 'aes-256-ctr';
1110 1185 $ivlen = openssl_cipher_iv_length( $method );
1111 1186 $iv = openssl_random_pseudo_bytes( $ivlen );
1112 1187
1113 - $raw_value = openssl_encrypt( $value . $salt, $method, $key, 0, $iv );
1114 - if ( ! $raw_value ) {
1188 + $ciphertext = openssl_encrypt( $value, $method, $key, OPENSSL_RAW_DATA, $iv );
1189 + if ( $ciphertext === false ) {
1115 1190 return false;
1116 1191 }
1117 1192
1118 - 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
1119 1196 }
1120 1197
1121 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 ) {
1122 1246
1123 1247 if(!$raw_value) {
1124 1248 return $raw_value;
1125 1249 }