Changeset 3488279
- Timestamp:
- 03/22/2026 04:01:56 PM (13 days ago)
- Location:
- sagepay-form-payment-gateway-for-gravity-forms/trunk
- Files:
-
- 3 edited
-
class-gf-sagepay-form.php (modified) (24 diffs)
-
readme.txt (modified) (2 diffs)
-
sagepay-form.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sagepay-form-payment-gateway-for-gravity-forms/trunk/class-gf-sagepay-form.php
r2888615 r3488279 99 99 'class' => 'medium', 100 100 'required' => true, 101 'tooltip' => '<h6>' . __( 'Encryption Password', 'gravityforms-sagepay-form' ) . '</h6>' . __( ' Please enter your encryption password provided by Opayo Form.', 'gravityforms-sagepay-form' ),101 'tooltip' => '<h6>' . __( 'Encryption Password', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'Enter the Form integration encryption password from MyOpayo (Administrator → Password details). It must match the environment (test/live). Opayo typically uses a 16-character password; shorter values are null-padded for AES as per Opayo’s PHP examples.', 'gravityforms-sagepay-form' ), 102 102 ), 103 103 array( … … 143 143 'required' => true, 144 144 'tooltip' => '<h6>' . __( 'Customer E-Mail Message', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'A message to the customer which is inserted into the successful transaction e-mails only.', 'gravityforms-sagepay-form' ), 145 ), /*145 ), 146 146 array( 147 147 'name' => 'apply3d', … … 164 164 'horizontal' => true, 165 165 'default_value' => '1', 166 'tooltip' => '<h6>' . __( 'Apply 3D Secure', 'gravityforms-sagepay-form' ) . '</h6>' . __( ' Select whether you would like to do 3D Secure Check on Transactions.', 'gravityforms-sagepay-form' ),167 ), */166 'tooltip' => '<h6>' . __( 'Apply 3D Secure', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'When enabled, Opayo applies its default 3D Secure rules. When disabled, 3D Secure authentication is not applied (Opayo Apply3DSecure = 2).', 'gravityforms-sagepay-form' ), 167 ), 168 168 array( 169 169 'name' => 'trans_type', 170 'label' => __( ' Send E-Mail', 'gravityforms-sagepay-form' ),170 'label' => __( 'Transaction type', 'gravityforms-sagepay-form' ), 171 171 'type' => 'radio', 172 172 'choices' => array( … … 191 191 'horizontal' => true, 192 192 'default_value' => 'PAYMENT', 193 'tooltip' => '<h6>' . __( ' Send E-Mails', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'Select Who to send e-mails to.', 'gravityforms-sagepay-form' ),193 'tooltip' => '<h6>' . __( 'Transaction type', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'PAYMENT captures funds immediately. DEFERRED authorises for later capture. AUTHENTICATE only validates the card.', 'gravityforms-sagepay-form' ), 194 194 ), 195 195 array( … … 278 278 // ------------------------------------------------------------------------------------------------- 279 279 280 // --add Page Style, Continue Button Label , Cancel URL280 // --add Page Style, Continue Button Label (Success/Cancel URLs use entry source_url + default GF confirmation) 281 281 $fields = array( 282 282 /* … … 297 297 'tooltip' => '<h6>' . __( 'Continue Button Label', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'Enter the text that should appear on the continue button once payment has been completed via SagePay Form.', 'gravityforms-sagepay-form' ) 298 298 ),*/ 299 array( 300 'name' => 'cancelUrl', 301 'label' => __( 'Cancel URL', 'gravityforms-sagepay-form' ), 302 'type' => 'text', 303 'class' => 'medium', 304 'required' => false, 305 'tooltip' => '<h6>' . __( 'Cancel URL', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'Enter the URL the user should be sent to should they cancel before completing their Opayo Form payment or payment fails.', 'gravityforms-sagepay-form' ), 306 ), 307 array( 308 'name' => 'successUrl', 309 'label' => __( 'Success URL', 'gravityforms-sagepay-form' ), 310 'type' => 'text', 311 'class' => 'medium', 312 'required' => false, 313 'tooltip' => '<h6>' . __( 'Success URL', 'gravityforms-sagepay-form' ) . '</h6>' . __( 'Enter the URL the user should be sent to after completing their Opayo Form payment.', 'gravityforms-sagepay-form' ), 314 ), /* 299 /* 315 300 array( 316 301 'name' => 'options', … … 693 678 public function redirect_url( $feed, $submission_data, $form, $entry ) { 694 679 695 696 $this->log_debug( '' ); 697 698 $this->log_debug( '' ); 699 700 $this->log_debug( '================================================================================================================' ); 701 702 $this->log_debug( '================================================================================================================' ); 703 704 $this->log_debug( 'Form Submission Data : {<pre>' . print_r( $entry, true ) . '</pre>}' ); 680 $this->log_debug( 'redirect_url: preparing Opayo Form redirect for entry #' . $entry['id'] ); 705 681 706 682 // Don't process redirect url if request is a Opayo Form return … … 711 687 $settings = $this->get_plugin_settings(); 712 688 689 $trans_type_setting = rgar( $settings, 'trans_type', 'PAYMENT' ); 690 713 691 $entry_check = GFAPI::get_entry( $entry['id'] ); 714 715 if( ( $entry_check['payment_status'] != '' || $entry_check['payment_status'] != 'Processing' ) && !empty($entry_check['transaction_id']) ){ 716 717 wp_die('<pre>Payment Failed please try again after sometime. Or contact support entry ID:'.$entry['id'].'</pre>'); 718 exit; 719 692 if ( is_wp_error( $entry_check ) ) { 693 $this->log_error( 'redirect_url: could not load entry #' . $entry['id'] . ' — ' . $entry_check->get_error_message() ); 694 return ''; 695 } 696 697 $pay_status = rgar( $entry_check, 'payment_status', '' ); 698 $transaction_id = rgar( $entry_check, 'transaction_id', '' ); 699 $final_statuses = array( 'Paid', 'Approved', 'Active' ); 700 701 if ( in_array( $pay_status, $final_statuses, true ) ) { 702 wp_die( 703 esc_html( 704 sprintf( 705 /* translators: %d: entry ID */ 706 __( 'This entry is already paid. Reference: %d', 'gravityforms-sagepay-form' ), 707 (int) $entry['id'] 708 ) 709 ), 710 esc_html__( 'Payment', 'gravityforms-sagepay-form' ), 711 array( 'response' => 400 ) 712 ); 713 } 714 715 // Block duplicate in-flight payment if a transaction id already exists while still "Processing" 716 if ( $pay_status === 'Processing' && $transaction_id !== '' && $transaction_id !== null ) { 717 wp_die( 718 esc_html( 719 sprintf( 720 /* translators: %d: entry ID */ 721 __( 'A payment is already in progress for this entry. Please wait or contact support. Reference: %d', 'gravityforms-sagepay-form' ), 722 (int) $entry['id'] 723 ) 724 ), 725 esc_html__( 'Payment', 'gravityforms-sagepay-form' ), 726 array( 'response' => 409 ) 727 ); 720 728 } 721 729 … … 723 731 GFAPI::update_entry_property( $entry['id'], 'payment_status', 'Processing' ); 724 732 725 if ( $ settings['trans_type']== 'PAYMENT' ) {733 if ( $trans_type_setting === 'PAYMENT' ) { 726 734 $trans_type = 'PAYMENT'; 727 } elseif ( $ settings['trans_type']== 'DEFERRED' ) {735 } elseif ( $trans_type_setting === 'DEFERRED' ) { 728 736 $trans_type = 'DEFERRED'; 729 737 } else { … … 743 751 $customer_fields = $this->customer_query_string( $feed, $entry ); 744 752 745 $return_url = $this->return_url( $form['id'], $entry['id'] );746 747 // Cancel URL748 $cancel_url = ! empty( $feed['meta']['cancelUrl'] ) ? $feed['meta']['cancelUrl'] : home_url();749 750 753 // URL that will listen to notifications from Opayo Form 751 754 $ipn_url = add_query_arg( 'page', 'gf_sagepay_form_ipn', home_url( '/' ) ); 752 755 753 $vendor_name = $settings['vendor_name']; 754 $mode = $settings['mode']; 755 //$apply3d = $settings['apply3d']; 756 $email_message = $settings['email_message']; 757 $send_emails = $settings['send_emails']; 758 $vendor_email = $settings['vendor_email']; 759 760 $custom_field = $entry['id'] . '|' . wp_hash( $entry['id'] ); 761 756 $failure_return_url = $this->get_opayo_cancel_return_url( $form, $entry ); 757 758 $vendor_name = $this->resolve_plugin_vendor_name( $settings ); 759 if ( $vendor_name === '' ) { 760 $this->log_error( 'Opayo Form: Vendor Name is empty. Set it under Forms → Settings → Opayo Form.' ); 761 wp_die( 762 esc_html__( 'The payment gateway is not fully configured (missing Opayo vendor name). Please contact the site administrator.', 'gravityforms-sagepay-form' ), 763 esc_html__( 'Payment unavailable', 'gravityforms-sagepay-form' ), 764 array( 'response' => 503 ) 765 ); 766 } 767 768 $apply3d = rgar( $settings, 'apply3d', '1' ); 769 $email_message = rgar( $settings, 'email_message', '' ); 770 $send_emails = rgar( $settings, 'send_emails', '2' ); 771 $vendor_email = rgar( $settings, 'vendor_email', '' ); 772 773 $payment_amount = false; 762 774 switch ( $feed['meta']['transactionType'] ) { 763 775 case 'product': … … 774 786 break; 775 787 */ 788 } 789 790 if ( $payment_amount === false || $payment_amount === '' || (float) $payment_amount <= 0 ) { 791 $this->log_debug( 'redirect_url: invalid or zero payment amount; not sending to Opayo.' ); 792 return ''; 776 793 } 777 794 … … 795 812 //$country = GFCommon::get_country_code( $submission_data['country'] ); 796 813 797 $sagepay_arg['VendorData'] = $entry[ $feed['meta']['billingInformation_vendor_data'] ]; 798 $sagepay_arg['ReferrerID'] = 'CC923B06-40D5-4713-85C1-700D690550BF'; 799 $sagepay_arg['Amount'] = $payment_amount; 814 $vendor_data_field_id = rgar( $feed['meta'], 'billingInformation_vendor_data' ); 815 $vendor_data = ( $vendor_data_field_id !== '' && $vendor_data_field_id !== null ) ? rgar( $entry, (string) $vendor_data_field_id, '' ) : ''; 816 if ( $vendor_data !== '' ) { 817 $sagepay_arg['VendorData'] = $this->sanitize_opayo_crypt_field_value( $vendor_data ); 818 } 819 820 $referrer_id = apply_filters( 'gform_sagepay_form_referrer_id', 'CC923B06-40D5-4713-85C1-700D690550BF', $form, $entry, $feed ); 821 if ( $referrer_id !== '' && $referrer_id !== null ) { 822 $sagepay_arg['ReferrerID'] = $this->sanitize_opayo_crypt_field_value( (string) $referrer_id ); 823 } 824 825 $sagepay_arg['Amount'] = $this->format_opayo_amount( $payment_amount ); 800 826 801 827 $sagepay_arg['CustomerName'] = substr( $first_name . ' ' . $last_name, 0, 100 ); … … 830 856 $sagepay_arg['DeliveryPhone'] = substr( $phone, 0, 20 ); 831 857 832 $sagepay_arg['FailureURL'] = $ipn_url;833 $sagepay_arg['SuccessURL'] = $ipn_url;858 $sagepay_arg['FailureURL'] = $failure_return_url; 859 $sagepay_arg['SuccessURL'] = $ipn_url; 834 860 $sagepay_arg['Description'] = sprintf( __( 'Order #%s', 'gravityforms' ), ltrim( $invoice, '#' ) ); 835 861 $sagepay_arg['Currency'] = $currency; 836 862 $sagepay_arg['VendorTxCode'] = $custom_field; 837 863 $sagepay_arg['VendorEMail'] = $vendor_email; 838 $sagepay_arg['SendEMail'] = $send_emails; 839 /* 840 if ( $apply3d != 0 ) { 841 $sagepay_arg['Apply3DSecure'] = 0;$apply3d; 842 }*/ 843 $sagepay_arg['Website'] = get_bloginfo( 'name' ); 864 $sagepay_arg['SendEMail'] = $send_emails; 865 // Opayo: 0 = apply 3DS per account/rules, 2 = do not apply 3DS 866 $sagepay_arg['Apply3DSecure'] = ( $apply3d === '1' || $apply3d === 1 || $apply3d === true ) ? 0 : 2; 867 $sagepay_arg['Website'] = get_bloginfo( 'name' ); 844 868 if ( $country == 'US' ) { 845 869 $sagepay_arg['eMailMessage'] = $email_message; 846 870 } 847 871 848 $post_values = ''; 872 $crypt_url_keys = array( 'FailureURL', 'SuccessURL' ); 873 $post_values = ''; 849 874 foreach ( $sagepay_arg as $key => $value ) { 850 $post_values .= "$key=" . trim( $value ) . '&'; 875 $raw = trim( (string) $value ); 876 $val = in_array( $key, $crypt_url_keys, true ) ? $this->sanitize_opayo_crypt_url_value( $raw ) : $this->sanitize_opayo_crypt_field_value( $raw ); 877 $post_values .= $key . '=' . $val . '&'; 851 878 } 852 879 $post_values = substr( $post_values, 0, -1 ); … … 855 882 $params['TxType'] = $trans_type; 856 883 $params['Vendor'] = $vendor_name; 857 $params['Crypt'] = $this->encryptAndEncode( $post_values ); 858 859 $query_string = http_build_query( $params ); 860 861 $query_string = apply_filters( "gform_sagepay_form_query_{$form['id']}", apply_filters( 'gform_sagepay_form_query', $query_string, $form, $entry, $feed ), $form, $entry, $feed ); 862 863 if ( ! $query_string ) { 864 $this->log_debug( 'NOT sending to Opayo Form: The price is either zero or the gform_sagepay_form_query filter was used to remove the querystring that is sent to Opayo Form.' ); 865 884 $params['Crypt'] = $this->encryptAndEncode( $post_values ); 885 886 if ( strlen( $params['Crypt'] ) < 32 ) { 887 $this->log_error( 'Opayo Form: Crypt payload is invalid or encryption failed; not redirecting.' ); 866 888 return ''; 867 889 } 868 890 869 $url = $ipn_url . '&' . $query_string; 891 $use_transient_bridge = apply_filters( 'gform_sagepay_form_use_transient_bridge', true, $params, $form, $entry, $feed ); 892 893 if ( $use_transient_bridge ) { 894 $params = apply_filters( 'gform_sagepay_form_bridge_params', $params, $form, $entry, $feed ); 895 if ( empty( $params['Crypt'] ) || empty( $params['Vendor'] ) ) { 896 $this->log_error( 'Opayo Form: bridge params invalid after gform_sagepay_form_bridge_params filter.' ); 897 return ''; 898 } 899 $token = wp_generate_password( 32, false, false ); 900 set_transient( $this->get_opayo_bridge_transient_key( $token ), $params, 15 * MINUTE_IN_SECONDS ); 901 $url = add_query_arg( 902 array( 903 'page' => 'gf_sagepay_form_ipn', 904 'opayo_br' => $token, 905 ), 906 home_url( '/' ) 907 ); 908 } else { 909 $query_string = http_build_query( $params ); 910 911 $query_string = apply_filters( "gform_sagepay_form_query_{$form['id']}", apply_filters( 'gform_sagepay_form_query', $query_string, $form, $entry, $feed ), $form, $entry, $feed ); 912 913 if ( ! $query_string ) { 914 $this->log_debug( 'NOT sending to Opayo Form: The price is either zero or the gform_sagepay_form_query filter was used to remove the querystring that is sent to Opayo Form.' ); 915 916 return ''; 917 } 918 919 $url = $ipn_url . '&' . $query_string; 920 } 870 921 871 922 $url = apply_filters( "gform_sagepay_form_request_{$form['id']}", apply_filters( 'gform_sagepay_form_request', $url, $form, $entry, $feed ), $form, $entry, $feed ); … … 878 929 } 879 930 931 /** 932 * Opayo Form AES-128-CBC: use a 16-byte key and IV derived from the Form encryption password (pad with nulls or truncate). 933 * OpenSSL applies PKCS#7 padding; do not pre-pad the plaintext (manual PKCS5 + OpenSSL caused double padding and 5080 errors). 934 */ 935 private function get_opayo_form_aes_key( $password ) { 936 $password = (string) $password; 937 return substr( str_pad( $password, 16, "\0" ), 0, 16 ); 938 } 939 940 /** 941 * Strip characters that break the Crypt name=value& string (newlines, control chars). 942 */ 943 private function sanitize_opayo_crypt_field_value( $value ) { 944 $value = preg_replace( '/[\x00-\x1F\x7F]/', ' ', (string) $value ); 945 // Plaintext is parsed as name=value pairs; & and = inside values break Opayo's decoder → 5080. 946 $value = str_replace( array( '&', '=' ), ' ', $value ); 947 return trim( preg_replace( '/\s+/', ' ', $value ) ); 948 } 949 950 /** 951 * URLs inside Crypt must keep query strings intact (&, =). 952 */ 953 private function sanitize_opayo_crypt_url_value( $value ) { 954 $value = preg_replace( '/[\x00-\x1F\x7F]/', ' ', (string) $value ); 955 return trim( $value ); 956 } 957 958 /** 959 * Page the customer submitted from (Gravity Forms entry source_url); used for Opayo cancel / failure redirect. 960 */ 961 private function get_opayo_cancel_return_url( $form, $entry ) { 962 $url = ''; 963 if ( is_array( $entry ) ) { 964 $url = trim( (string) rgar( $entry, 'source_url', '' ) ); 965 } 966 $parsed = is_string( $url ) && $url !== '' ? wp_parse_url( $url ) : array(); 967 if ( ! empty( $parsed['scheme'] ) && in_array( strtolower( $parsed['scheme'] ), array( 'http', 'https' ), true ) ) { 968 return apply_filters( 'gform_sagepay_form_cancel_return_url', $url, $form, $entry ); 969 } 970 971 return apply_filters( 'gform_sagepay_form_cancel_return_url', home_url( '/' ), $form, $entry ); 972 } 973 974 /** 975 * After Opayo returns, send the customer to the form page with gf_sagepay_form_return so GFFormDisplay::handle_confirmation runs (default GF confirmation / redirect). 976 */ 977 private function get_opayo_success_return_url( $form, $entry ) { 978 $base = $this->get_opayo_cancel_return_url( $form, $entry ); 979 $form_id = (int) rgar( $entry, 'form_id' ); 980 $lead_id = (int) rgar( $entry, 'id' ); 981 if ( $form_id < 1 || $lead_id < 1 ) { 982 return apply_filters( 'gform_sagepay_form_success_return_url', $base, $form, $entry ); 983 } 984 $ids_query = "ids={$form_id}|{$lead_id}"; 985 $ids_query .= '&hash=' . wp_hash( $ids_query ); 986 $url = add_query_arg( 'gf_sagepay_form_return', base64_encode( $ids_query ), $base ); 987 988 return apply_filters( 'gform_sagepay_form_success_return_url', $url, $form, $entry ); 989 } 990 991 /** 992 * Transient key for server-side bridge (avoids huge Crypt in GET, which is often truncated → 5080). 993 */ 994 private function get_opayo_bridge_transient_key( $token ) { 995 return 'gfspf_opayo_' . $token; 996 } 997 998 private function get_opayo_form_register_url( $mode ) { 999 if ( $mode !== 'live' ) { 1000 $url = 'https://sandbox.opayo.eu.elavon.com/gateway/service/vspform-register.vsp'; 1001 } else { 1002 $url = 'https://live.opayo.eu.elavon.com/gateway/service/vspform-register.vsp'; 1003 } 1004 return apply_filters( 'gform_sagepay_form_gateway_register_url', $url, $mode ); 1005 } 1006 1007 /** 1008 * Output auto-POST HTML to Opayo vspform-register. 1009 * 1010 * @param array $fields Assoc: VPSProtocol, TxType, Vendor, Crypt. 1011 * @param string $register_url Gateway URL. 1012 */ 1013 private function output_opayo_register_post_document( $fields, $register_url ) { 1014 if ( ! is_array( $fields ) ) { 1015 $fields = array(); 1016 } 1017 $html_inputs = array(); 1018 foreach ( $fields as $key => $value ) { 1019 if ( is_array( $value ) ) { 1020 continue; 1021 } 1022 $html_inputs[] = '<input type="hidden" name="' . esc_attr( (string) $key ) . '" value="' . esc_attr( (string) $value ) . '" />'; 1023 } 1024 1025 echo '<!DOCTYPE html> 1026 <html> 1027 <head> 1028 <meta charset="utf-8" /> 1029 <script> 1030 window.onload = function() { document.getElementById("sagepay_payment_form").submit(); }; 1031 </script> 1032 </head> 1033 <body> 1034 <form action="' . esc_url( $register_url ) . '" method="post" name="sagepay_payment_form" id="sagepay_payment_form"> 1035 ' . implode( '', $html_inputs ) . ' 1036 <noscript><p><input type="submit" value="' . esc_attr__( 'Continue to payment', 'gravityforms-sagepay-form' ) . '" /></p></noscript> 1037 <p>' . esc_html__( 'Please wait while you are redirected to the payment page.', 'gravityforms-sagepay-form' ) . '</p> 1038 </form> 1039 </body> 1040 </html>'; 1041 } 1042 1043 /** 1044 * Opayo expects amount as d.cc (e.g. 10.00), no thousands separators. 1045 */ 1046 private function format_opayo_amount( $amount ) { 1047 return number_format( (float) $amount, 2, '.', '' ); 1048 } 1049 1050 /** 1051 * Resolve Opayo vendor name from add-on settings, legacy options, or bad upgrade shapes. 1052 */ 1053 private function resolve_plugin_vendor_name( $settings ) { 1054 if ( ! is_array( $settings ) ) { 1055 $settings = array(); 1056 } 1057 1058 $candidates = array(); 1059 $candidates[] = rgar( $settings, 'vendor_name', '' ); 1060 1061 $nested = rgar( $settings, 'gf_sagepay_form_configured' ); 1062 if ( is_array( $nested ) ) { 1063 $candidates[] = rgar( $nested, 'vendor_name', '' ); 1064 $candidates[] = rgar( $nested, 'vendor', '' ); 1065 } 1066 1067 $legacy = get_option( 'gf_sagepay_form_configured', null ); 1068 if ( is_array( $legacy ) ) { 1069 $candidates[] = rgar( $legacy, 'vendor_name', '' ); 1070 $candidates[] = rgar( $legacy, 'vendor', '' ); 1071 } 1072 1073 foreach ( $candidates as $v ) { 1074 if ( ! is_string( $v ) ) { 1075 continue; 1076 } 1077 $v = trim( $v ); 1078 if ( $v !== '' ) { 1079 return apply_filters( 'gform_sagepay_form_vendor_name', $v, $settings ); 1080 } 1081 } 1082 1083 return apply_filters( 'gform_sagepay_form_vendor_name', '', $settings ); 1084 } 1085 880 1086 public function encryptAndEncode( $strIn ) { 881 1087 882 1088 $settings = $this->get_plugin_settings(); 883 $vendorpass = $settings['vendor_password']; 884 $strIn = self::pkcs5_pad( $strIn, 16 ); 885 886 if ( PHP_VERSION >= 7 ) { 887 return '@' . bin2hex( openssl_encrypt( $strIn, 'AES-128-CBC', $vendorpass, OPENSSL_RAW_DATA, $vendorpass ) ); 888 } else { 889 return '@' . bin2hex( mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $vendorpass, $strIn, MCRYPT_MODE_CBC, $vendorpass ) ); 890 } 1089 $vendorpass = rgar( $settings, 'vendor_password', '' ); 1090 $key = $this->get_opayo_form_aes_key( $vendorpass ); 1091 1092 if ( version_compare( PHP_VERSION, '7.0.0', '>=' ) ) { 1093 $crypt = openssl_encrypt( (string) $strIn, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $key ); 1094 if ( false === $crypt ) { 1095 $this->log_error( 'Opayo Form: openssl_encrypt failed. Check the Form encryption password in settings and OpenSSL support.' ); 1096 return '@'; 1097 } 1098 return '@' . strtoupper( bin2hex( $crypt ) ); 1099 } 1100 1101 $strIn_padded = self::pkcs5_pad( (string) $strIn, 16 ); 1102 return '@' . strtoupper( bin2hex( mcrypt_encrypt( MCRYPT_RIJNDAEL_128, $key, $strIn_padded, MCRYPT_MODE_CBC, $key ) ) ); 891 1103 } 892 1104 893 1105 public function decodeAndDecrypt( $strIn ) { 894 1106 $settings = $this->get_plugin_settings(); 895 $vendorpass = $settings['vendor_password']; 896 $strIn = substr( $strIn, 1 ); 897 $strIn = pack( 'H*', $strIn ); 898 899 if ( PHP_VERSION >= 7 ) { 900 return openssl_decrypt( $strIn, 'AES-128-CBC', $vendorpass, OPENSSL_RAW_DATA, $vendorpass ); 901 } else { 902 return mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $vendorpass, $strIn, MCRYPT_MODE_CBC, $vendorpass ); 903 } 1107 $vendorpass = rgar( $settings, 'vendor_password', '' ); 1108 $key = $this->get_opayo_form_aes_key( $vendorpass ); 1109 $hex = substr( $strIn, 1 ); 1110 if ( '' === $hex || 0 !== strlen( $hex ) % 2 ) { 1111 return ''; 1112 } 1113 $binary = function_exists( 'hex2bin' ) ? hex2bin( $hex ) : pack( 'H*', $hex ); 1114 if ( false === $binary ) { 1115 return ''; 1116 } 1117 1118 if ( version_compare( PHP_VERSION, '7.0.0', '>=' ) ) { 1119 $plain = openssl_decrypt( $binary, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $key ); 1120 return ( false !== $plain && null !== $plain ) ? $plain : ''; 1121 } 1122 1123 return mcrypt_decrypt( MCRYPT_RIJNDAEL_128, $key, $binary, MCRYPT_MODE_CBC, $key ); 904 1124 } 905 1125 … … 912 1132 public function decode( $strIn ) { 913 1133 $decodedString = self::decodeAndDecrypt( $strIn ); 1134 if ( ! is_string( $decodedString ) || $decodedString === '' ) { 1135 return array(); 1136 } 1137 $sagePayResponse = array(); 914 1138 parse_str( $decodedString, $sagePayResponse ); 915 1139 return $sagePayResponse; … … 1400 1624 1401 1625 1402 // ------- PROCESSING WORLDPAYIPN (Callback) -----------//1626 // ------- Opayo Form IPN (Callback) -----------// 1403 1627 1404 1628 public function callback() { … … 1408 1632 } 1409 1633 1634 $bridge_token = isset( $_GET['opayo_br'] ) ? sanitize_text_field( wp_unslash( $_GET['opayo_br'] ) ) : ''; 1635 if ( $bridge_token !== '' && preg_match( '/^[A-Za-z0-9]{20,48}$/', $bridge_token ) ) { 1636 $settings = $this->get_plugin_settings(); 1637 $mode = rgar( $settings, 'mode', 'test' ); 1638 $trans_key = $this->get_opayo_bridge_transient_key( $bridge_token ); 1639 $params = get_transient( $trans_key ); 1640 delete_transient( $trans_key ); 1641 1642 if ( false === $params || ! is_array( $params ) || empty( $params['Crypt'] ) ) { 1643 wp_die( 1644 esc_html__( 'This payment session has expired or is invalid. Please submit the form again.', 'gravityforms-sagepay-form' ), 1645 esc_html__( 'Payment session expired', 'gravityforms-sagepay-form' ), 1646 array( 'response' => 410 ) 1647 ); 1648 } 1649 1650 $this->output_opayo_register_post_document( $params, $this->get_opayo_form_register_url( $mode ) ); 1651 exit; 1652 } 1653 1410 1654 if ( isset( $_GET['VPSProtocol'] ) ) { 1411 unset( $_GET['page'] );1412 1413 1655 $settings = $this->get_plugin_settings(); 1414 1415 // Getting Url (Live or Sandbox) 1416 if ( $settings['mode'] != 'live' ) { 1417 $redirect_url = 'https://test.sagepay.com/gateway/service/vspform-register.vsp'; 1418 } else { 1419 $redirect_url = 'https://live.sagepay.com/gateway/service/vspform-register.vsp'; 1420 } 1421 1422 $sagepay_arg_array = array(); 1423 1424 foreach ( $_GET as $key => $value ) { 1425 $sagepay_arg_array[] = '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />'; 1426 } 1427 1428 echo '<!DOCTYPE html> 1429 <html> 1430 <head> 1431 <script> 1432 window.onload = function(e){ 1433 document.getElementById("sagepay_payment_form").submit(); 1434 } 1435 </script> 1436 </head> 1437 <body> 1438 <form action="' . $redirect_url . '" method="post" name="sagepay_payment_form" target="_self" id="sagepay_payment_form" > 1439 ' . implode( '', $sagepay_arg_array ) . ' 1440 <b> Please wait while you are being redirected.</b> 1441 </form> 1442 </body> 1443 </html>'; 1444 1656 $mode = rgar( $settings, 'mode', 'test' ); 1657 1658 $redirect_url = $this->get_opayo_form_register_url( $mode ); 1659 1660 $bridge = wp_unslash( $_GET ); 1661 unset( $bridge['page'] ); 1662 1663 $resolved_vendor = $this->resolve_plugin_vendor_name( $settings ); 1664 if ( ( ! isset( $bridge['Vendor'] ) || '' === (string) $bridge['Vendor'] ) && $resolved_vendor !== '' ) { 1665 $bridge['Vendor'] = $resolved_vendor; 1666 } 1667 if ( $resolved_vendor === '' && ( ! isset( $bridge['Vendor'] ) || '' === (string) $bridge['Vendor'] ) ) { 1668 wp_die( 1669 esc_html__( 'Payment gateway error: Opayo vendor name is not configured.', 'gravityforms-sagepay-form' ), 1670 esc_html__( 'Payment unavailable', 'gravityforms-sagepay-form' ), 1671 array( 'response' => 503 ) 1672 ); 1673 } 1674 1675 $this->output_opayo_register_post_document( $bridge, $redirect_url ); 1445 1676 exit; 1446 1677 … … 1449 1680 $this->log_debug( 'IPN request received. Starting to process...' ); 1450 1681 1451 $this->log_debug( 'IPN Response : '.print_r( $_REQUEST, true ) ); 1452 1453 $this->log_debug( print_r( $transaction_response, true ) ); 1454 1455 $transaction_response = $this->decode( str_replace( ' ', '+', $_REQUEST['crypt'] ) ); 1456 1457 $this->log_debug( print_r( $transaction_response, true ) ); 1458 1459 list($entry_id, $hash) = explode( '-', $transaction_response['VendorTxCode'] ); 1460 1461 $hash = wp_hash( $entry_id ); 1682 if ( rgempty( 'crypt', $_REQUEST ) ) { 1683 $this->log_error( 'IPN missing crypt parameter. Aborting.' ); 1684 return false; 1685 } 1686 1687 $crypt_raw = wp_unslash( $_REQUEST['crypt'] ); 1688 $transaction_response = $this->decode( str_replace( ' ', '+', $crypt_raw ) ); 1689 1690 if ( empty( $transaction_response ) || ! is_array( $transaction_response ) ) { 1691 $this->log_error( 'IPN could not decode crypt payload. Aborting.' ); 1692 return false; 1693 } 1694 1695 $this->log_debug( 'IPN decoded response: ' . print_r( $transaction_response, true ) ); 1696 1697 if ( empty( $transaction_response['VendorTxCode'] ) ) { 1698 $this->log_error( 'IPN missing VendorTxCode after decode. Aborting.' ); 1699 return false; 1700 } 1701 1702 $vendor_tx = (string) $transaction_response['VendorTxCode']; 1703 $dash_pos = strpos( $vendor_tx, '-' ); 1704 $entry_id = $dash_pos !== false ? substr( $vendor_tx, 0, $dash_pos ) : $vendor_tx; 1705 $entry_id = trim( $entry_id ); 1706 1707 if ( $entry_id === '' || ! ctype_digit( $entry_id ) ) { 1708 $this->log_error( 'IPN VendorTxCode does not start with a numeric entry ID. Aborting.' ); 1709 return false; 1710 } 1462 1711 1463 1712 // ------ Getting entry related to this IPN ----------------------------------------------// 1464 $entry = $this->get_entry( $entry_id . '|' . $hash);1713 $entry = $this->get_entry( $entry_id . '|' . wp_hash( $entry_id ) ); 1465 1714 1466 1715 // Ignore orphan IPN messages (ones without an entry) 1467 1716 if ( ! $entry ) { 1468 $this->log_error( 'Entry could not be found. Entry ID: ' . $entry ['id']. '. Aborting.' );1717 $this->log_error( 'Entry could not be found. Entry ID: ' . $entry_id . '. Aborting.' ); 1469 1718 1470 1719 return false; … … 1612 1861 $this->complete_payment( $entry, $action ); 1613 1862 1614 $redirect_url = ! empty( $config['meta']['successUrl'] ) ? $config['meta']['successUrl'] : home_url(); 1615 wp_redirect( $redirect_url ); 1863 $form = GFAPI::get_form( rgar( $entry, 'form_id' ) ); 1864 $redirect_url = $this->get_opayo_success_return_url( is_array( $form ) ? $form : array(), $entry ); 1865 wp_safe_redirect( $redirect_url ); 1616 1866 exit; 1617 1867 … … 1629 1879 $action['note'] = sprintf( __( 'Payment is pending. Amount: %1$s. Transaction Id: %2$s. Reason: %3$s', 'gravityforms-sagepay-form' ), $amount_formatted, $action['transaction_id'], $this->get_pending_reason( $pending_reason ) ); 1630 1880 1631 $redirect_url = ! empty( $config['meta']['successUrl'] ) ? $config['meta']['successUrl'] : home_url(); 1632 wp_redirect( $redirect_url ); 1881 $form = GFAPI::get_form( rgar( $entry, 'form_id' ) ); 1882 $redirect_url = $this->get_opayo_success_return_url( is_array( $form ) ? $form : array(), $entry ); 1883 wp_safe_redirect( $redirect_url ); 1633 1884 exit; 1634 1885 … … 1659 1910 } 1660 1911 1661 $redirect_url = ! empty( $config['meta']['cancelUrl'] ) ? $config['meta']['cancelUrl'] : home_url(); 1662 wp_redirect( $redirect_url ); 1912 $form = GFAPI::get_form( rgar( $entry, 'form_id' ) ); 1913 $redirect_url = $this->get_opayo_cancel_return_url( is_array( $form ) ? $form : array(), $entry ); 1914 wp_safe_redirect( $redirect_url ); 1663 1915 exit; 1664 1916 … … 1679 1931 1680 1932 // Getting entry associated with this IPN message (entry id is sent in the 'custom' field) 1681 list( $entry_id, $hash ) = explode( '|', $custom_field ); 1933 $parts = explode( '|', $custom_field, 2 ); 1934 if ( count( $parts ) < 2 ) { 1935 $this->log_error( "IPN custom field malformed (expected entry_id|hash): {$custom_field}. Aborting." ); 1936 return false; 1937 } 1938 list( $entry_id, $hash ) = $parts; 1682 1939 $hash_matches = wp_hash( $entry_id ) == $hash; 1683 1940 … … 2256 2513 2257 2514 public function copy_settings() { 2258 // copy plugin settings2259 2515 $old_settings = get_option( 'gf_sagepay_form_configured' ); 2260 $new_settings = array( 'gf_sagepay_form_configured' => $old_settings ); 2261 $this->update_plugin_settings( $new_settings ); 2516 if ( ! is_array( $old_settings ) ) { 2517 return; 2518 } 2519 $current = $this->get_plugin_settings(); 2520 if ( ! is_array( $current ) ) { 2521 $current = array(); 2522 } 2523 // Merge legacy flat settings into the add-on option (do not nest under gf_sagepay_form_configured). 2524 $this->update_plugin_settings( array_merge( $old_settings, $current ) ); 2262 2525 } 2263 2526 -
sagepay-form-payment-gateway-for-gravity-forms/trunk/readme.txt
r2888620 r3488279 1 1 === Opayo Form Payment Gateway for Gravity Forms === 2 2 Contributors: patsatech 3 Tags: ecommerce, payment gateway, wordpress,gravity forms,Opayo server,Opayo go4 Requires at least: 3.55 Tested up to: 6. 1.16 Stable tag: 1. 1.93 Tags: ecommerce, payment gateway, gravity forms,Opayo server,Opayo go 4 Requires at least: 4.5 5 Tested up to: 6.9.4 6 Stable tag: 1.2.0 7 7 License: GPLv2 or later 8 8 9 Opayo Server Gateway for accepting payments on your Gravity Forms Store.9 Accept card payments in Gravity Forms using Opayo Form (hosted checkout by Elavon)—customers pay on Opayo’s pages, not on your server. 10 10 11 11 == Description == 12 12 13 Th e Sage Pay Payment system provides a secure, simple means of authorizing credit and debit card transactions from your website.13 This add-on connects **Gravity Forms** to **Opayo Form** (formerly Sage Pay Form), Elavon’s hosted payment integration. Shoppers are sent to Opayo to enter card details; card data never touches your WordPress site, which keeps your PCI scope lower than with on-page card fields. 14 14 15 The Sage Pay system provides a straightforward payment interface for the customer, and takes complete responsibility for the online transaction, including the collection and encrypted storage of credit and debit card details, eliminating the security implications of holding such sensitive information on your own servers. 15 **You need** 16 16 17 So this plugin helps you to accept payments with Gravity Forms using Opayo Accounts. 18 Send us your ideas and feedback here: https://www.patsatech.com/contact-us 17 * WordPress 4.5 or higher and a supported PHP version (7.0+ recommended). 18 * **Gravity Forms** with a license that includes payment add-ons (this plugin extends the Gravity Forms payment framework). 19 * An **Opayo** account with **Form** integration enabled, plus your vendor name and Form encryption password from **MyOpayo**. 20 21 **What it supports** 22 23 * Product and donation feeds, billing field mapping, and delayed notifications or post creation until payment completes (where you configure it in the feed). 24 * Opayo **VPS Protocol 4.00**, test and live modes, and current Elavon gateway hosts (`sandbox.opayo.eu.elavon.com` / `live.opayo.eu.elavon.com`), with a filter to override URLs if your integration pack specifies different endpoints. 25 * Reliable handoff to Opayo using a short on-site URL plus a transient (avoids oversized query strings that can cause registration errors). 26 * After payment, customers can be returned to the form’s page so Gravity Forms can run the normal **confirmation** (message or redirect), using the entry’s source URL when available. 27 28 **Support and feedback** 29 30 Questions or ideas: [contact PatSaTECH](https://www.patsatech.com/contact-us). 19 31 20 32 == Installation == 21 33 22 1. Download and unzip the latest release zip file. 23 2. If you use the WordPress plugin uploader to install this plugin skip to step 4. 24 3. Upload the entire plugin directory to your `/wp-content/plugins/` directory. 25 4. Activate the plugin through the 'Plugins' menu in WordPress Administration. 34 1. **Install Gravity Forms** (if it is not already installed) and ensure your license supports payment add-ons. 35 36 2. **Install this plugin** 37 * In WordPress: **Plugins → Add New → Upload Plugin**, choose the ZIP, then **Install Now**, **Activate**; or 38 * Upload the plugin folder to `wp-content/plugins/` via FTP/SFTP, then activate under **Plugins**. 39 40 3. **Configure Opayo (global)** 41 Go to **Forms → Settings → Opayo Form** (or the Opayo Form item under Gravity Forms settings). Enter **Vendor name**, **Encryption password**, **Vendor email**, choose **Test** or **Live**, set **Transaction type** (e.g. PAYMENT), and save. Use the credentials from **MyOpayo** for the same environment (test vs live). 42 43 4. **Add a payment feed to a form** 44 Open your form → **Settings → Opayo Form** → add a feed. Map billing fields, choose products or donations, and adjust notifications/posts options as needed. There is no separate “Cancel URL” or “Success URL” field: cancel uses the entry **source URL** (the page where the form was submitted), and success returns through Gravity Forms’ normal confirmation flow. 45 46 5. **Test** 47 Use Opayo **test/sandbox** mode and a test card until you are satisfied, then switch to **Live** and verify with a small real transaction. 48 49 For gateway-specific rules, limits, and 3D Secure behaviour, refer to **Opayo / Elavon** documentation and your MyOpayo account. 26 50 27 51 == Changelog == … … 60 84 = 1.1.9 = 61 85 * Updated to change branding from Sagepaay to Opayo. 86 87 = 1.2.0 = 88 * Hardened IPN/callback handling (validate crypt, decode before logging, safer VendorTxCode parsing, correct error when entry is missing). 89 * Fixed duplicate-payment guard when redirecting to Opayo (avoid blocking legitimate submissions). 90 * Restored Apply 3D Secure global setting and send Apply3DSecure in the Crypt payload. 91 * Fixed mislabeled transaction type setting; use version_compare for OpenSSL vs mcrypt. 92 * Added filter gform_sagepay_form_gateway_register_url to override test/live registration URLs if Opayo changes endpoints. 93 * Synced plugin version constant with readme; trimmed noisy debug logging on redirect. 94 * Default Form registration URLs now use Elavon Opayo hosts (sandbox.opayo.eu.elavon.com / live.opayo.eu.elavon.com) instead of legacy test.sagepay.com / live.sagepay.com. Same path: /gateway/service/vspform-register.vsp. Use filter gform_sagepay_form_gateway_register_url to point at legacy or alternate URLs if required. 95 * Fixed Opayo Form Crypt encryption for PHP OpenSSL: removed double padding (manual PKCS5 plus OpenSSL PKCS7), which commonly caused error 5080 “Form transaction registration failed”. Encryption now matches Opayo’s documented approach (16-byte key/IV from password, uppercase hex). 96 * Amount sent as formatted decimal (e.g. 10.00). Sanitize Crypt field values (control characters). Safer VendorData mapping; optional ReferrerID via filter gform_sagepay_form_referrer_id. 97 * Resolve Opayo Vendor from add-on settings, legacy gf_sagepay_form_configured option, or wrongly nested upgrade data; filter gform_sagepay_form_vendor_name. 98 * Block checkout if vendor name is still missing (clear error instead of posting blank Vendor to Opayo). 99 * On the bridge page, reinject Vendor from saved settings when the query string omits it; restore hidden fields and auto-submit (removed debug text inputs / disabled submit). 100 * Upgrade copy_settings now merges legacy settings flat instead of nesting them under gf_sagepay_form_configured. 101 * Default payment handoff uses a short URL + server-side transient, then POSTs to Opayo (avoids huge Crypt in the query string, which is often truncated by servers/proxies and causes 5080). 102 * Strip & and = from values inside the encrypted Crypt payload (they break Opayo’s name=value parsing). 103 * Filters: gform_sagepay_form_use_transient_bridge (default true), gform_sagepay_form_bridge_params. -
sagepay-form-payment-gateway-for-gravity-forms/trunk/sagepay-form.php
r2888615 r3488279 4 4 * Plugin URI: http://www.patsatech.com/ 5 5 * Description: Integrates Gravity Forms with Opayo Form Method, enabling end users to purchase goods and services through Gravity Forms. 6 * Version: 1. 1.96 * Version: 1.2.0 7 7 * Author: PatSaTECH 8 8 * Author URI: http://www.patsatech.com 9 9 * Contributors: patsatech 10 10 * Requires at least: 4.5 11 * Tested up to: 6. 1.111 * Tested up to: 6.9.4 12 12 * 13 13 * Text Domain: gravityforms-sagepay-form … … 18 18 */ 19 19 20 define( 'GF_SAGEPAY_FORM_VERSION', '1. 1.7' );20 define( 'GF_SAGEPAY_FORM_VERSION', '1.2.0' ); 21 21 22 22 add_action( 'gform_loaded', array( 'GF_SagePay_Form_Bootstrap', 'load' ), 5 );
Note: See TracChangeset
for help on using the changeset viewer.