├── assets └── images │ └── icon.png ├── changelog.txt ├── gateway-payfast.php ├── includes ├── class-wc-gateway-payfast-blocks-support.php ├── class-wc-gateway-payfast-privacy.php └── class-wc-gateway-payfast.php ├── readme.txt ├── src └── blocks │ └── payment-method │ ├── constants.js │ ├── index.js │ └── payfast-utils.js ├── woocommerce-gateway-payfast.php └── wordpress_org_assets ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png ├── icon-160x160.png └── icon-256x256.png /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-payfast/885af62bc98bb0f5b5f01e2a0a69368bf377874c/assets/images/icon.png -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | *** PayFast Changelog *** 2 | 3 | = 1.7.1 - 2025-05-05 = 4 | * Fix - PHP warning for undefined variable when running alongside WooPayments. 5 | * Dev - Bump WooCommerce "tested up to" version 9.8. 6 | * Dev - Bump WooCommerce minimum supported version to 9.6. 7 | * Dev - Bump WordPress "tested up to" version 6.8. 8 | * Dev - Bump WordPress minimum supported version to 6.6. 9 | * Dev - Update all third-party actions our workflows rely on to use versions based on specific commit hashes. 10 | 11 | = 1.7.0 - 2025-03-17 = 12 | * Update - Refresh copy and brand assets. 13 | * Dev - Bump WooCommerce "tested up to" version 9.7. 14 | * Dev - Bump WooCommerce minimum supported version to 9.5. 15 | * Dev - Bump WordPress minimum supported version to 6.6. 16 | * Dev - Add the WordPress Plugin Check GitHub Action and fix all issues it found. 17 | 18 | = 1.6.10 - 2025-01-13 = 19 | * Dev - Bump WooCommerce "tested up to" version 9.6. 20 | * Dev - Bump WooCommerce minimum supported version to 9.4. 21 | * Dev - Use the `@woocommerce/e2e-utils-playwright` NPM package for E2E tests. 22 | 23 | = 1.6.9 - 2024-11-18 = 24 | * Dev - Bump WordPress "tested up to" version 6.7. 25 | 26 | = 1.6.8 - 2024-11-04 = 27 | * Add - Credentials validation and required field notice for PayFast in the sandbox environment. 28 | * Dev - Bump WooCommerce "tested up to" version 9.4. 29 | * Dev - Bump WooCommerce minimum supported version to 9.2. 30 | * Dev - Bump WordPress minimum supported version to 6.5. 31 | 32 | = 1.6.7 - 2024-09-09 = 33 | * Dev - Bump WooCommerce "tested up to" version 9.3. 34 | * Dev - Bump WooCommerce minimum supported version to 9.1. 35 | * Dev - Update E2E tests to accommodate changes in WooCommerce. 36 | 37 | = 1.6.6 - 2024-07-29 = 38 | * Dev - Bump WooCommerce "tested up to" version 9.1. 39 | * Dev - Bump WooCommerce minimum supported version to 8.9. 40 | * Dev - Bump WordPress "tested up to" version 6.6. 41 | * Dev - Bump WordPress minimum supported version to 6.4. 42 | * Dev - Update NPM packages and node version to v20 to modernize developer experience. 43 | * Dev - Exclude the Woo Comment Hook `@since` sniff. 44 | * Dev - Fix QIT E2E tests and add support for a few new test types. 45 | * Tweak - Update WordPress.org plugin assets. 46 | 47 | = 1.6.5 - 2024-05-14 = 48 | * Fix - Use `rawurlencode` around the call to `get_site_url` to ensure things are encoded properly. 49 | 50 | = 1.6.4 - 2024-05-07 = 51 | * Fix - Resolved signature mismatch error caused by HTML entity encoding in site/blog name. 52 | * Dev - Bump WooCommerce "tested up to" version 8.8. 53 | * Dev - Bump WooCommerce minimum supported version to 8.6. 54 | 55 | = 1.6.3 - 2024-05-02 = 56 | * Fix - Enforce amount match check for all payments in the Payfast ITN handler. 57 | * Dev - Bump WooCommerce "tested up to" version 8.7. 58 | * Dev - Bump WooCommerce minimum supported version to 8.5. 59 | * Dev - Bump WordPress "tested up to" version 6.5. 60 | * Dev - Bump WordPress minimum supported version to 6.3. 61 | 62 | = 1.6.2 - 2024-03-25 = 63 | * Dev - Bump WooCommerce "tested up to" version 8.6. 64 | * Dev - Bump WooCommerce minimum supported version to 8.4. 65 | * Dev - Bump WordPress minimum supported version to 6.3. 66 | * Fix - Payfast gateway not visible on Checkout when ZAR currency is set via WooPayments multi-currency feature. 67 | * Fix - Allow navigation back from PayFast gateway payment page. 68 | 69 | = 1.6.1 - 2024-01-08 = 70 | * Add - Readme.md file for e2e tests. 71 | * Dev - Declare compatibility with WooCommerce Blocks. 72 | * Dev - Declare compatibility with Product Editor. 73 | * Dev - Updated the main file of the plugin to match the plugin's slug. 74 | * Dev - Bump PHP minimum supported version to 7.4. 75 | * Dev - Bump WooCommerce "tested up to" version 8.4. 76 | * Dev - Bump WooCommerce minimum supported version to 8.2. 77 | * Dev - Resolve coding standards issues. 78 | * Tweak - Bump PHP "tested up to" version 8.3. 79 | 80 | = 1.6.0 - 2023-11-22 = 81 | * Dev - Add Playwright end-to-end tests. 82 | * Dev - Update default behavior to use a block-based cart and checkout in E2E tests. 83 | * Dev - Bump WooCommerce "tested up to" version 8.3. 84 | * Dev - Bump WooCommerce minimum supported version to 8.1. 85 | * Dev - Bump WordPress minimum supported version to 6.2. 86 | * Dev - Bump WordPress "tested up to" version 6.4. 87 | * Dev - Bump WordPress minimum supported version to 6.2. 88 | * Dev - Update PHPCS and PHPCompatibility GitHub Actions. 89 | 90 | = 1.5.9 - 2023-09-18 = 91 | * Dev - Bump WordPress "tested up to" version from 6.2 to 6.3. 92 | * Dev - Bump WooCommerce "tested up to" version 7.9. 93 | * Dev - Bump WooCommerce minimum supported version to 7.7. 94 | * Dev - Bump PHP minimum supported version to 7.3. 95 | 96 | = 1.5.8 - 2023-08-29 = 97 | * Add - Admin notice if this extension is activated without WooCommerce. 98 | 99 | = 1.5.7 - 2023-07-25 = 100 | * Fix - Handle WP_Error object when return from wp_remote_request. 101 | 102 | = 1.5.6 - 2023-07-19 = 103 | * Fix - Include build directory. 104 | 105 | = 1.5.5 - 2023-07-04 = 106 | * Dev - Bump WooCommerce "tested up to" version 7.8. 107 | * Dev - Bump WooCommerce minimum supported version from 6.8 to 7.2. 108 | * Dev - Bump WordPress minimum supported version from 5.8 to 6.1. 109 | * Fix - Replace escaping of order total price elements on the edit order admin screen. 110 | 111 | = 1.5.4 - 2023-06-13 = 112 | * Fix - Escaped strings. 113 | 114 | = 1.5.3 - 2023-05-25 = 115 | * Dev – Bump WooCommerce “tested up to” version 7.6. 116 | * Dev – Bump WordPress minimum supported version from 5.6 to 5.8. 117 | * Dev – Bump WordPress “tested up to” version 6.2. 118 | 119 | = 1.5.2 - 2023-03-16 = 120 | * Tweak - Bump PHP minimum supported version from 7.0 to 7.2. 121 | * Tweak - Bump WooCommerce minimum supported version from 6.0 to 6.8. 122 | * Tweak - Bump WooCommerce "tested up to" version 7.4. 123 | 124 | = 1.5.1 - 2023-02-28 = 125 | * Update – Payfast logo and text references to meet their new branding guidelines. 126 | * Tweak – Bump WooCommerce “Tested up to” to 7.3. 127 | * Tweak – Bump WooCommerce tested up to 7.3.0. 128 | * Dev – Bump @sideway/formula from 3.0.0 to 3.0.1. 129 | * Dev – Resolved linting issues. 130 | * Dev – Bump json5 from 1.0.1 to 1.0.2. 131 | * Dev – Bump loader-utils from 1.4.0 to 1.4.2. 132 | 133 | = 1.5.0 - 2022-12-06 = 134 | * Add – Support for High-performance Order Storage (“HPOS”) (formerly known as Custom Order Tables, “COT”). 135 | * Dev – Update node version from 12.0.0 to 16.13.0. 136 | * Dev – Update npm version from 6.9.0 to 8.0.0. 137 | * Tweak – Bump minimum PHP version from 5.6 to 7.0. 138 | * Tweak – Bump minimum WP version from 4.4 to 5.6. 139 | * Tweak – Bump minimum WC version from 2.6 to 6.0. 140 | 141 | = 1.4.25 - 2022-09-07 = 142 | * Fix - Add support for Transaction ID. 143 | 144 | = 1.4.24 - 2022-07-19 = 145 | * Fix - Subscription renewal payment failed issue in the production environment. 146 | 147 | = 1.4.23 - 2022-07-05 = 148 | * Add - Allow setup PayFast during onboarding. 149 | * Add - Added support for customer subscription payment method change. 150 | 151 | = 1.4.22 - 2022-05-12 = 152 | * Tweak - WP tested up to 6.0 153 | 154 | = 1.4.21 - 2022-05-03 = 155 | * Tweak - Bump tested up to WordPress version 5.9. 156 | 157 | = 1.4.20 - 2022-01-18 = 158 | * Fix - Status toggle button not working as expected 159 | 160 | = 1.4.19 - 2021-05-04 = 161 | * Add - support for the Cart and Checkout blocks included 162 | * Fix - Error notice from direct access to the order id property. 163 | 164 | = 1.4.18 - 2021-02-04 = 165 | * Add fees to order 166 | * Add signature to the request to PayFast 167 | * Tweak - WC 4.9.2 compatibility. 168 | * Tweak - WP 5.6 compatibility. 169 | 170 | = 1.4.17 - 2020-11-25 = 171 | * Fix - Fix Object could not be converted to string when renewing a subscription. 172 | * Tweak - WC tested up to 4.7 173 | * Tweak - WP tested up to 5.6 174 | * Tweak - PHP 8.0 compatibility. 175 | 176 | = 1.4.15 - 2020-03-30 = 177 | * Tweak - WC tested up to 4.0 178 | * Tweak - WP tested up to 5.4 179 | 180 | = 1.4.14 - 2019-10-24 = 181 | * Fix - Incorrect API response handling for subscription renewal payments. 182 | * Tweak - WC tested up to 3.8 183 | * Tweak - WP tested up to 5.3 184 | 185 | = 1.4.13 - 2019-08-06 = 186 | * Tweak - WC tested up to 3.7 187 | 188 | = 1.4.12 - 2019-04-16 = 189 | * Tweak - WC tested up to 3.6 190 | 191 | = 1.4.11 - 2018-11-19 = 192 | * Update - WP tested up to 5.0 193 | 194 | = 1.4.10 - 2018-09-26 = 195 | * Update - WC tested up to 3.5 196 | 197 | = 1.4.9 - 2018-05-22 = 198 | * Update - WC tested up to 3.4 199 | * Update - Privacy policy notification. 200 | * Update - Export/erasure hooks added. 201 | 202 | = 1.4.8 - 2018-05-01 = 203 | * Tweak - Add support for X-Forwarded-For header. 204 | 205 | = 1.4.7 - 2017-12-13 = 206 | * Tweak - Adjusts the headers to indicate WooCommerce 3.3 compatibility. 207 | * New - Adds a filter around the supported currencies. 208 | * Tweak - Replaces "Pricing Options" with "General Settings" in the "disabled" admin notice. 209 | 210 | = 1.4.6 - 2017-11-23 = 211 | * Fix - Error on admin setting pages due to wrong static method call. 212 | 213 | = 1.4.5 - 2017-11-22 = 214 | * Tweak - Remove unneeded order information from the plugin log. 215 | * Tweak - Passphrase now required. 216 | * New - filter to override the is valid ip check. 217 | 218 | = 1.4.4 - 2017-06-14 = 219 | * Fix - Add additional error handling on the PayFast API. 220 | * Add - Option to enable logging. 221 | 222 | = 1.4.3 - 2017-05-03 = 223 | * Fix - Allow users to update card details when paying manually for a failed subscription 224 | * Fix - Renewal orders on new subscriptions are stuck in Pending status even though the payment went through 225 | * Fix - ITN debug emails are slightly messed up wrt new line characters 226 | 227 | = 1.4.2 - 2017-04-19 = 228 | * Fix - Fatal error on renewing subscription. 229 | * Fix - Additional updates for WooCommerce 3.0 compatibility. 230 | 231 | = 1.4.1 - 2017-04-03 = 232 | * Fix - Update for WooCommerce 3.0 compatibility 233 | 234 | = 1.4.0 - 2016-09-02 = 235 | * Add - Support for Subscriptions 236 | * Add - Support for Pre-Orders 237 | 238 | = 1.3.1 - 2016-01-14 = 239 | * Fix - Description field use on the checkout page 240 | * Add - Helpful links on the plugin page 241 | 242 | = 1.3.0 - 2015-10-23 = 243 | * New - Add email_address to the transaction information sent through to PayFast. 244 | 245 | = 1.2.9 - 2015-08-07 = 246 | * Fix - Removes "empty()" call when outputting the description field. 247 | 248 | = 1.2.8 - 2015-08-04 = 249 | * Fix - Fixes the notices displayed in admin when defining constants. 250 | * Fix - Ensures the gateway displays correctly on checkout when in sandbox mode, even if no merchant credentials are yet present. 251 | * Code tidy. 252 | * Removed legacy code. 253 | * Switched to WC logging class. 254 | 255 | = 1.2.7 - 2015-04-20 = 256 | * Fix - Corrects the plugin textdomain. 257 | * Fix - Corrects the incorrect use of the non-existent sanitize() function. 258 | 259 | = 1.2.6 - 2014-01-13 = 260 | * WC 2.1 Compatibility 261 | 262 | = 1.2.5 - 2013-09-06 = 263 | * Update PayFast Logo 264 | 265 | = 1.2.4 - 2013-08-29 = 266 | * Sequential Order Numbers support 267 | * Better WooCommerce version tracking for PayFast 268 | 269 | = 1.2.3 - 2013-06-26 = 270 | * Track WooCommerce version 271 | 272 | = 1.2.2 - 2013-01-21 = 273 | * WC 2.0 Compat 274 | 275 | = 1.2.1 - 2012-12-05 = 276 | * Updater 277 | 278 | = 1.2 - 2012-10-23 = 279 | * Class names 280 | 281 | = 1.1 - 2012-06-18 = 282 | * Patched release with new class names and Woo updater 283 | 284 | = 1.0 - 2011-10-06 = 285 | * First Release 286 | -------------------------------------------------------------------------------- /gateway-payfast.php: -------------------------------------------------------------------------------- 1 | $active_plugin ) { 16 | if ( strstr( $active_plugin, '/gateway-payfast.php' ) ) { 17 | $active_plugins[ $key ] = str_replace( '/gateway-payfast.php', '/woocommerce-gateway-payfast.php', $active_plugin ); 18 | } 19 | } 20 | 21 | update_option( 'active_plugins', $active_plugins ); 22 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-payfast-blocks-support.php: -------------------------------------------------------------------------------- 1 | settings = get_option( 'woocommerce_payfast_settings', array() ); 28 | } 29 | 30 | /** 31 | * Returns if this payment method should be active. If false, the scripts will not be enqueued. 32 | * 33 | * @return boolean 34 | */ 35 | public function is_active() { 36 | $payment_gateways_class = WC()->payment_gateways(); 37 | $payment_gateways = $payment_gateways_class->payment_gateways(); 38 | 39 | return $payment_gateways['payfast']->is_available(); 40 | } 41 | 42 | /** 43 | * Returns an array of scripts/handles to be registered for this payment method. 44 | * 45 | * @return array 46 | */ 47 | public function get_payment_method_script_handles() { 48 | $asset_path = WC_GATEWAY_PAYFAST_PATH . '/build/payment-method.asset.php'; 49 | $version = WC_GATEWAY_PAYFAST_VERSION; 50 | $dependencies = array(); 51 | if ( file_exists( $asset_path ) ) { 52 | $asset = require $asset_path; 53 | $version = is_array( $asset ) && isset( $asset['version'] ) 54 | ? $asset['version'] 55 | : $version; 56 | $dependencies = is_array( $asset ) && isset( $asset['dependencies'] ) 57 | ? $asset['dependencies'] 58 | : $dependencies; 59 | } 60 | wp_register_script( 61 | 'wc-payfast-blocks-integration', 62 | WC_GATEWAY_PAYFAST_URL . '/build/payment-method.js', 63 | $dependencies, 64 | $version, 65 | true 66 | ); 67 | wp_set_script_translations( 68 | 'wc-payfast-blocks-integration', 69 | 'woocommerce-gateway-payfast' 70 | ); 71 | return array( 'wc-payfast-blocks-integration' ); 72 | } 73 | 74 | /** 75 | * Returns an array of key=>value pairs of data made available to the payment methods script. 76 | * 77 | * @return array 78 | */ 79 | public function get_payment_method_data() { 80 | return array( 81 | 'title' => $this->get_setting( 'title' ), 82 | 'description' => $this->get_setting( 'description' ), 83 | 'supports' => $this->get_supported_features(), 84 | 'logo_url' => WC_GATEWAY_PAYFAST_URL . '/assets/images/icon.png', 85 | ); 86 | } 87 | 88 | /** 89 | * Returns an array of supported features. 90 | * 91 | * @return string[] 92 | */ 93 | public function get_supported_features() { 94 | $payment_gateways = WC()->payment_gateways->payment_gateways(); 95 | return $payment_gateways['payfast']->supports; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-payfast-privacy.php: -------------------------------------------------------------------------------- 1 | add_exporter( 'woocommerce-gateway-payfast-order-data', __( 'WooCommerce Payfast Order Data', 'woocommerce-gateway-payfast' ), array( $this, 'order_data_exporter' ) ); 23 | 24 | if ( function_exists( 'wcs_get_subscriptions' ) ) { 25 | $this->add_exporter( 'woocommerce-gateway-payfast-subscriptions-data', __( 'WooCommerce Payfast Subscriptions Data', 'woocommerce-gateway-payfast' ), array( $this, 'subscriptions_data_exporter' ) ); 26 | } 27 | 28 | $this->add_eraser( 'woocommerce-gateway-payfast-order-data', __( 'WooCommerce Payfast Data', 'woocommerce-gateway-payfast' ), array( $this, 'order_data_eraser' ) ); 29 | } 30 | 31 | /** 32 | * Returns a list of orders that are using one of Payfast's payment methods. 33 | * 34 | * The list of orders is paginated to 10 orders per page. 35 | * 36 | * @param string $email_address The user email address. 37 | * @param int $page Page number to query. 38 | * @return WC_Order[]|stdClass Number of pages and an array of order objects. 39 | */ 40 | protected function get_payfast_orders( $email_address, $page ) { 41 | $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. 42 | 43 | $order_query = array( 44 | 'payment_method' => 'payfast', 45 | 'limit' => 10, 46 | 'page' => $page, 47 | ); 48 | 49 | if ( $user instanceof WP_User ) { 50 | $order_query['customer_id'] = (int) $user->ID; 51 | } else { 52 | $order_query['billing_email'] = $email_address; 53 | } 54 | 55 | return wc_get_orders( $order_query ); 56 | } 57 | 58 | /** 59 | * Gets the message of the privacy to display. 60 | */ 61 | public function get_privacy_message() { 62 | return wpautop( 63 | sprintf( 64 | /* translators: 1: anchor tag 2: closing anchor tag */ 65 | esc_html__( 'By using this extension, you may be storing personal data or sharing data with an external service. %1$sLearn more about how this works, including what you may want to include in your privacy policy.%2$s', 'woocommerce-gateway-payfast' ), 66 | '', 67 | '' 68 | ) 69 | ); 70 | } 71 | 72 | /** 73 | * Handle exporting data for Orders. 74 | * 75 | * @param string $email_address E-mail address to export. 76 | * @param int $page Pagination of data. 77 | * 78 | * @return array 79 | */ 80 | public function order_data_exporter( $email_address, $page = 1 ) { 81 | $done = false; 82 | $data_to_export = array(); 83 | 84 | $orders = $this->get_payfast_orders( $email_address, (int) $page ); 85 | 86 | $done = true; 87 | 88 | if ( 0 < count( $orders ) ) { 89 | foreach ( $orders as $order ) { 90 | $data_to_export[] = array( 91 | 'group_id' => 'woocommerce_orders', 92 | 'group_label' => esc_attr__( 'Orders', 'woocommerce-gateway-payfast' ), 93 | 'item_id' => 'order-' . $order->get_id(), 94 | 'data' => array( 95 | array( 96 | 'name' => esc_attr__( 'Payfast token', 'woocommerce-gateway-payfast' ), 97 | 'value' => $order->get_meta( '_payfast_pre_order_token', true ), 98 | ), 99 | ), 100 | ); 101 | } 102 | 103 | $done = 10 > count( $orders ); 104 | } 105 | 106 | return array( 107 | 'data' => $data_to_export, 108 | 'done' => $done, 109 | ); 110 | } 111 | 112 | /** 113 | * Handle exporting data for Subscriptions. 114 | * 115 | * @param string $email_address E-mail address to export. 116 | * @param int $page Pagination of data. 117 | * 118 | * @return array 119 | */ 120 | public function subscriptions_data_exporter( $email_address, $page = 1 ) { 121 | $done = false; 122 | $page = (int) $page; 123 | $data_to_export = array(); 124 | 125 | $meta_query = array( 126 | 'relation' => 'AND', 127 | array( 128 | 'key' => '_payment_method', 129 | 'value' => 'payfast', 130 | 'compare' => '=', 131 | ), 132 | array( 133 | 'key' => '_billing_email', 134 | 'value' => $email_address, 135 | 'compare' => '=', 136 | ), 137 | ); 138 | 139 | $subscription_query = array( 140 | 'posts_per_page' => 10, 141 | 'page' => $page, 142 | // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 143 | 'meta_query' => $meta_query, 144 | ); 145 | 146 | $subscriptions = wcs_get_subscriptions( $subscription_query ); 147 | 148 | $done = true; 149 | 150 | if ( 0 < count( $subscriptions ) ) { 151 | foreach ( $subscriptions as $subscription ) { 152 | $data_to_export[] = array( 153 | 'group_id' => 'woocommerce_subscriptions', 154 | 'group_label' => esc_attr__( 'Subscriptions', 'woocommerce-gateway-payfast' ), 155 | 'item_id' => 'subscription-' . $subscription->get_id(), 156 | 'data' => array( 157 | array( 158 | 'name' => esc_attr__( 'Payfast subscription token', 'woocommerce-gateway-payfast' ), 159 | 'value' => $subscription->get_meta( '_payfast_subscription_token', true ), 160 | ), 161 | ), 162 | ); 163 | } 164 | 165 | $done = 10 > count( $subscriptions ); 166 | } 167 | 168 | return array( 169 | 'data' => $data_to_export, 170 | 'done' => $done, 171 | ); 172 | } 173 | 174 | /** 175 | * Finds and erases order data by email address. 176 | * 177 | * @since 3.4.0 178 | * @param string $email_address The user email address. 179 | * @param int $page Page. 180 | * @return array An array of personal data in name value pairs 181 | */ 182 | public function order_data_eraser( $email_address, $page ) { 183 | $orders = $this->get_payfast_orders( $email_address, (int) $page ); 184 | 185 | $items_removed = false; 186 | $items_retained = false; 187 | $messages = array(); 188 | 189 | foreach ( (array) $orders as $order ) { 190 | $order = wc_get_order( $order->get_id() ); 191 | 192 | list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); 193 | $items_removed |= $removed; 194 | $items_retained |= $retained; 195 | $messages = array_merge( $messages, $msgs ); 196 | 197 | list( $removed, $retained, $msgs ) = $this->maybe_handle_subscription( $order ); 198 | $items_removed |= $removed; 199 | $items_retained |= $retained; 200 | $messages = array_merge( $messages, $msgs ); 201 | } 202 | 203 | // Tell core if we have more orders to work on still. 204 | $done = count( $orders ) < 10; 205 | 206 | return array( 207 | 'items_removed' => $items_removed, 208 | 'items_retained' => $items_retained, 209 | 'messages' => $messages, 210 | 'done' => $done, 211 | ); 212 | } 213 | 214 | /** 215 | * Handle eraser of data tied to Subscriptions 216 | * 217 | * @param WC_Order $order Order object. 218 | * @return array 219 | */ 220 | protected function maybe_handle_subscription( $order ) { 221 | if ( ! class_exists( 'WC_Subscriptions' ) ) { 222 | return array( false, false, array() ); 223 | } 224 | 225 | if ( ! wcs_order_contains_subscription( $order ) ) { 226 | return array( false, false, array() ); 227 | } 228 | 229 | $subscription = current( wcs_get_subscriptions_for_order( $order->get_id() ) ); 230 | 231 | $payfast_source_id = $subscription->get_meta( '_payfast_subscription_token', true ); 232 | 233 | if ( empty( $payfast_source_id ) ) { 234 | return array( false, false, array() ); 235 | } 236 | 237 | /** 238 | * Filter privacy eraser subscription statuses. 239 | * 240 | * Modify the subscription statuses that are considered active and should be retained. 241 | * 242 | * @since 1.4.13 243 | * 244 | * @param string[] $statuses Array of subscription statuses considered active. 245 | */ 246 | if ( $subscription->has_status( apply_filters( 'wc_payfast_privacy_eraser_subs_statuses', array( 'on-hold', 'active' ) ) ) ) { 247 | return array( 248 | false, 249 | true, 250 | array( 251 | sprintf( 252 | /* translators: %d: Order ID */ 253 | esc_html__( 'Order ID %d contains an active Subscription', 'woocommerce-gateway-payfast' ), 254 | $order->get_id() 255 | ), 256 | ), 257 | ); 258 | } 259 | 260 | $renewal_orders = WC_Subscriptions_Renewal_Order::get_renewal_orders( $order->get_id(), 'WC_Order' ); 261 | 262 | foreach ( $renewal_orders as $renewal_order ) { 263 | $renewal_order->delete_meta_data( '_payfast_subscription_token' ); 264 | $renewal_order->save_meta_data(); 265 | } 266 | 267 | $subscription->delete_meta_data( '_payfast_subscription_token' ); 268 | $subscription->save_meta_data(); 269 | 270 | return array( true, false, array( esc_html__( 'Payfast Subscriptions Data Erased.', 'woocommerce-gateway-payfast' ) ) ); 271 | } 272 | 273 | /** 274 | * Handle eraser of data tied to Orders 275 | * 276 | * @since 1.4.13 277 | * 278 | * @param WC_Order $order The order object. 279 | * @return array 280 | */ 281 | protected function maybe_handle_order( $order ) { 282 | $payfast_token = $order->get_meta( '_payfast_pre_order_token', true ); 283 | 284 | if ( empty( $payfast_token ) ) { 285 | return array( false, false, array() ); 286 | } 287 | 288 | $order->delete_meta_data( '_payfast_pre_order_token' ); 289 | $order->save_meta_data(); 290 | 291 | return array( true, false, array( esc_html__( 'Payfast Order Data Erased.', 'woocommerce-gateway-payfast' ) ) ); 292 | } 293 | } 294 | 295 | new WC_Gateway_PayFast_Privacy(); 296 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-payfast.php: -------------------------------------------------------------------------------- 1 | version = WC_GATEWAY_PAYFAST_VERSION; 120 | $this->id = 'payfast'; 121 | $this->method_title = __( 'Payfast', 'woocommerce-gateway-payfast' ); 122 | /* translators: 1: a href link 2: closing href */ 123 | $this->method_description = sprintf( __( 'Payfast works by sending the user to %1$sPayfast%2$s to enter their payment information.', 'woocommerce-gateway-payfast' ), '', '' ); 124 | $this->icon = WP_PLUGIN_URL . '/' . plugin_basename( dirname( __DIR__ ) ) . '/assets/images/icon.png'; 125 | $this->debug_email = get_option( 'admin_email' ); 126 | $this->available_countries = array( 'ZA' ); 127 | 128 | /** 129 | * Filter available countries for Payfast Gateway. 130 | * 131 | * @since 1.4.13 132 | * 133 | * @param string[] $available_countries Array of available countries. 134 | */ 135 | $this->available_currencies = (array) apply_filters( 'woocommerce_gateway_payfast_available_currencies', array( 'ZAR' ) ); 136 | 137 | // Supported functionality. 138 | $this->supports = array( 139 | 'products', 140 | 'subscriptions', 141 | 'subscription_cancellation', 142 | 'subscription_suspension', 143 | 'subscription_reactivation', 144 | 'subscription_amount_changes', 145 | 'subscription_date_changes', 146 | 'subscription_payment_method_change', // Subs 1.x support. 147 | 'subscription_payment_method_change_customer', // Enabled for https://github.com/woocommerce/woocommerce-gateway-payfast/issues/32. 148 | ); 149 | 150 | $this->init_form_fields(); 151 | $this->init_settings(); 152 | 153 | if ( ! is_admin() ) { 154 | $this->setup_constants(); 155 | } 156 | 157 | // Setup default merchant data. 158 | $this->merchant_id = $this->get_option( 'merchant_id' ); 159 | $this->merchant_key = $this->get_option( 'merchant_key' ); 160 | $this->pass_phrase = $this->get_option( 'pass_phrase' ); 161 | $this->url = 'https://www.payfast.co.za/eng/process?aff=woo-free'; 162 | $this->validate_url = 'https://www.payfast.co.za/eng/query/validate'; 163 | $this->title = $this->get_option( 'title' ); 164 | $this->response_url = add_query_arg( 'wc-api', 'WC_Gateway_PayFast', home_url( '/' ) ); 165 | $this->send_debug_email = 'yes' === $this->get_option( 'send_debug_email' ); 166 | $this->description = $this->get_option( 'description' ); 167 | $this->enabled = 'yes' === $this->get_option( 'enabled' ) ? 'yes' : 'no'; 168 | $this->enable_logging = 'yes' === $this->get_option( 'enable_logging' ); 169 | 170 | // Setup the test data, if in test mode. 171 | if ( 'yes' === $this->get_option( 'testmode' ) ) { 172 | $this->url = 'https://sandbox.payfast.co.za/eng/process?aff=woo-free'; 173 | $this->validate_url = 'https://sandbox.payfast.co.za/eng/query/validate'; 174 | $this->add_testmode_admin_settings_notice(); 175 | } else { 176 | $this->send_debug_email = false; 177 | } 178 | 179 | add_action( 'woocommerce_api_wc_gateway_payfast', array( $this, 'check_itn_response' ) ); 180 | add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); 181 | add_action( 'woocommerce_receipt_payfast', array( $this, 'receipt_page' ) ); 182 | add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 ); 183 | add_action( 'woocommerce_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); 184 | add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 185 | 186 | // Add fees to order. 187 | add_action( 'woocommerce_admin_order_totals_after_total', array( $this, 'display_order_fee' ) ); 188 | add_action( 'woocommerce_admin_order_totals_after_total', array( $this, 'display_order_net' ), 20 ); 189 | 190 | // Change Payment Method actions. 191 | add_action( 'woocommerce_subscription_payment_method_updated_from_' . $this->id, array( $this, 'maybe_cancel_subscription_token' ), 10, 2 ); 192 | 193 | // Add support for WooPayments multi-currency. 194 | add_filter( 'woocommerce_currency', array( $this, 'filter_currency' ) ); 195 | 196 | add_filter( 'nocache_headers', array( $this, 'no_store_cache_headers' ) ); 197 | 198 | // Validate the gateway credentials. 199 | add_action( 'update_option_woocommerce_payfast_settings', array( $this, 'validate_payfast_credentials' ), 10, 2 ); 200 | } 201 | 202 | /** 203 | * Use the no-store, private cache directive on the order-pay endpoint. 204 | * 205 | * This prevents the browser caching the page even when the visitor has clicked 206 | * the back button. This is required to determine if a user has pressed back while 207 | * in the payfast gateway. 208 | * 209 | * @since 1.6.2 210 | * 211 | * @param string[] $headers Array of caching headers. 212 | * @return string[] Modified caching headers. 213 | */ 214 | public function no_store_cache_headers( $headers ) { 215 | if ( ! is_wc_endpoint_url( 'order-pay' ) ) { 216 | return $headers; 217 | } 218 | 219 | $headers['Cache-Control'] = 'no-cache, must-revalidate, max-age=0, no-store, private'; 220 | return $headers; 221 | } 222 | 223 | /** 224 | * Initialise Gateway Settings Form Fields 225 | * 226 | * @since 1.0.0 227 | */ 228 | public function init_form_fields() { 229 | $this->form_fields = array( 230 | 'enabled' => array( 231 | 'title' => __( 'Enable/Disable', 'woocommerce-gateway-payfast' ), 232 | 'label' => __( 'Enable Payfast', 'woocommerce-gateway-payfast' ), 233 | 'type' => 'checkbox', 234 | 'description' => __( 'This controls whether or not this gateway is enabled within WooCommerce.', 'woocommerce-gateway-payfast' ), 235 | 'default' => 'no', // User should enter the required information before enabling the gateway. 236 | 'desc_tip' => true, 237 | ), 238 | 'title' => array( 239 | 'title' => __( 'Title', 'woocommerce-gateway-payfast' ), 240 | 'type' => 'text', 241 | 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-payfast' ), 242 | 'default' => __( 'Payfast', 'woocommerce-gateway-payfast' ), 243 | 'desc_tip' => true, 244 | ), 245 | 'description' => array( 246 | 'title' => __( 'Description', 'woocommerce-gateway-payfast' ), 247 | 'type' => 'text', 248 | 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-payfast' ), 249 | 'default' => '', 250 | 'desc_tip' => true, 251 | ), 252 | 'testmode' => array( 253 | 'title' => __( 'Payfast Sandbox', 'woocommerce-gateway-payfast' ), 254 | 'type' => 'checkbox', 255 | 'description' => __( 'Place the payment gateway in development mode.', 'woocommerce-gateway-payfast' ), 256 | 'default' => 'yes', 257 | ), 258 | 'merchant_id' => array( 259 | 'title' => __( 'Merchant ID', 'woocommerce-gateway-payfast' ), 260 | 'type' => 'text', 261 | 'description' => __( 'This is the merchant ID, received from Payfast.', 'woocommerce-gateway-payfast' ), 262 | 'default' => '', 263 | ), 264 | 'merchant_key' => array( 265 | 'title' => __( 'Merchant Key', 'woocommerce-gateway-payfast' ), 266 | 'type' => 'text', 267 | 'description' => __( 'This is the merchant key, received from Payfast.', 'woocommerce-gateway-payfast' ), 268 | 'default' => '', 269 | ), 270 | 'pass_phrase' => array( 271 | 'title' => __( 'Passphrase', 'woocommerce-gateway-payfast' ), 272 | 'type' => 'text', 273 | 'description' => __( '* Required. Needed to ensure the data passed through is secure.', 'woocommerce-gateway-payfast' ), 274 | 'default' => '', 275 | ), 276 | 'send_debug_email' => array( 277 | 'title' => __( 'Send Debug Emails', 'woocommerce-gateway-payfast' ), 278 | 'type' => 'checkbox', 279 | 'label' => __( 'Send debug e-mails for transactions through the Payfast gateway (sends on successful transaction as well).', 'woocommerce-gateway-payfast' ), 280 | 'default' => 'yes', 281 | ), 282 | 'debug_email' => array( 283 | 'title' => __( 'Who Receives Debug E-mails?', 'woocommerce-gateway-payfast' ), 284 | 'type' => 'text', 285 | 'description' => __( 'The e-mail address to which debugging error e-mails are sent when in test mode.', 'woocommerce-gateway-payfast' ), 286 | 'default' => get_option( 'admin_email' ), 287 | ), 288 | 'enable_logging' => array( 289 | 'title' => __( 'Enable Logging', 'woocommerce-gateway-payfast' ), 290 | 'type' => 'checkbox', 291 | 'label' => __( 'Enable transaction logging for gateway.', 'woocommerce-gateway-payfast' ), 292 | 'default' => 'no', 293 | ), 294 | ); 295 | } 296 | 297 | /** 298 | * Get the required form field keys for setup. 299 | * 300 | * @return array 301 | */ 302 | public function get_required_settings_keys() { 303 | return array( 304 | 'merchant_id', 305 | 'merchant_key', 306 | 'pass_phrase', 307 | ); 308 | } 309 | 310 | /** 311 | * Determine if the gateway still requires setup. 312 | * 313 | * @return bool 314 | */ 315 | public function needs_setup() { 316 | return ! $this->get_option( 'merchant_id' ) || ! $this->get_option( 'merchant_key' ) || ! $this->get_option( 'pass_phrase' ); 317 | } 318 | 319 | /** 320 | * Add a notice to the merchant_key and merchant_id fields when in test mode. 321 | * 322 | * @since 1.0.0 323 | */ 324 | public function add_testmode_admin_settings_notice() { 325 | $this->form_fields['merchant_id']['description'] .= ' ' . esc_html__( 'Sandbox Merchant ID currently in use', 'woocommerce-gateway-payfast' ) . ' ( ' . esc_html( $this->merchant_id ) . ' ).'; 326 | $this->form_fields['merchant_key']['description'] .= ' ' . esc_html__( 'Sandbox Merchant Key currently in use', 'woocommerce-gateway-payfast' ) . ' ( ' . esc_html( $this->merchant_key ) . ' ).'; 327 | } 328 | 329 | /** 330 | * Check if this gateway is enabled and available in the base currency being traded with. 331 | * 332 | * @since 1.0.0 333 | * @return array 334 | */ 335 | public function check_requirements() { 336 | $errors = array( 337 | // Check if the store currency is supported by Payfast. 338 | ! in_array( get_woocommerce_currency(), $this->available_currencies, true ) ? 'wc-gateway-payfast-error-invalid-currency' : null, 339 | // Check if user entered the merchant ID. 340 | empty( $this->get_option( 'merchant_id' ) ) ? 'wc-gateway-payfast-error-missing-merchant-id' : null, 341 | // Check if user entered the merchant key. 342 | empty( $this->get_option( 'merchant_key' ) ) ? 'wc-gateway-payfast-error-missing-merchant-key' : null, 343 | // Check if user entered a pass phrase. 344 | empty( $this->get_option( 'pass_phrase' ) ) ? 'wc-gateway-payfast-error-missing-pass-phrase' : null, 345 | // Check if payfast credentials are valid. 346 | ( 'yes' === get_option( 'woocommerce_payfast_invalid_credentials' ) ) ? 'wc-gateway-payfast-error-invalid-credentials' : null, 347 | ); 348 | 349 | return array_filter( $errors ); 350 | } 351 | 352 | /** 353 | * Check if the gateway is available for use. 354 | * 355 | * @return bool 356 | */ 357 | public function is_available() { 358 | if ( 'yes' === $this->enabled ) { 359 | $errors = $this->check_requirements(); 360 | // Prevent using this gateway on frontend if there are any configuration errors. 361 | return 0 === count( $errors ); 362 | } 363 | 364 | return parent::is_available(); 365 | } 366 | 367 | /** 368 | * Admin Panel Options 369 | * - Options for bits like 'title' and availability on a country-by-country basis 370 | * 371 | * @since 1.0.0 372 | */ 373 | public function admin_options() { 374 | if ( in_array( get_woocommerce_currency(), $this->available_currencies, true ) ) { 375 | parent::admin_options(); 376 | } else { 377 | ?> 378 |
379 |381 | 382 | ', '' ) ); 385 | ?> 386 |
387 |' . esc_html__( 'Thank you for your order, please click the button below to pay with Payfast.', 'woocommerce-gateway-payfast' ) . '
'; 599 | $this->generate_payfast_form( $order ); 600 | } 601 | 602 | /** 603 | * Check Payfast ITN response. 604 | * 605 | * @since 1.0.0 606 | */ 607 | public function check_itn_response() { 608 | // phpcs:ignore.WordPress.Security.NonceVerification.Missing 609 | $this->handle_itn_request( stripslashes_deep( $_POST ) ); 610 | 611 | // Notify Payfast that information has been received. 612 | header( 'HTTP/1.0 200 OK' ); 613 | flush(); 614 | } 615 | 616 | /** 617 | * Check Payfast ITN validity. 618 | * 619 | * @param array $data Data. 620 | * @since 1.0.0 621 | */ 622 | public function handle_itn_request( $data ) { 623 | $this->log( 624 | PHP_EOL 625 | . '----------' 626 | . PHP_EOL . 'Payfast ITN call received' 627 | . PHP_EOL . '----------' 628 | ); 629 | $this->log( 'Get posted data' ); 630 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- debug info for logging. 631 | $this->log( 'Payfast Data: ' . print_r( $data, true ) ); 632 | 633 | $payfast_error = false; 634 | $payfast_done = false; 635 | $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); 636 | $session_id = $data['custom_str1']; 637 | $vendor_name = get_bloginfo( 'name', 'display' ); 638 | $vendor_url = home_url( '/' ); 639 | $order_id = absint( $data['custom_str3'] ); 640 | $order_key = wc_clean( $session_id ); 641 | $order = wc_get_order( $order_id ); 642 | $original_order = $order; 643 | 644 | if ( false === $data ) { 645 | $payfast_error = true; 646 | $payfast_error_message = PF_ERR_BAD_ACCESS; 647 | } 648 | 649 | // Verify security signature. 650 | if ( ! $payfast_error && ! $payfast_done ) { 651 | $this->log( 'Verify security signature' ); 652 | $signature = md5( $this->_generate_parameter_string( $data, false, false ) ); // false not to sort data. 653 | // If signature different, log for debugging. 654 | if ( ! $this->validate_signature( $data, $signature ) ) { 655 | $payfast_error = true; 656 | $payfast_error_message = PF_ERR_INVALID_SIGNATURE; 657 | } 658 | } 659 | 660 | // Verify source IP (If not in debug mode). 661 | if ( ! $payfast_error && ! $payfast_done 662 | && $this->get_option( 'testmode' ) !== 'yes' ) { 663 | $this->log( 'Verify source IP' ); 664 | 665 | if ( isset( $_SERVER['REMOTE_ADDR'] ) && ! $this->is_valid_ip( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) ) { 666 | $payfast_error = true; 667 | $payfast_error_message = PF_ERR_BAD_SOURCE_IP; 668 | } 669 | } 670 | 671 | // Verify data received. 672 | if ( ! $payfast_error ) { 673 | $this->log( 'Verify data received' ); 674 | $validation_data = $data; 675 | unset( $validation_data['signature'] ); 676 | $has_valid_response_data = $this->validate_response_data( $validation_data ); 677 | 678 | if ( ! $has_valid_response_data ) { 679 | $payfast_error = true; 680 | $payfast_error_message = PF_ERR_BAD_ACCESS; 681 | } 682 | } 683 | 684 | /** 685 | * Handle Changing Payment Method. 686 | * - Save payfast subscription token to handle future payment 687 | * - (for Payfast to Payfast payment method change) Cancel old token, as future payment will be handle with new token 688 | * 689 | * Note: The change payment method is handled before the amount mismatch check, as it doesn't involve an actual payment (0.00) and only token updates are handled here. 690 | */ 691 | if ( 692 | ! $payfast_error && 693 | isset( $data['custom_str4'] ) && 694 | 'change_pay_method' === wc_clean( $data['custom_str4'] ) && 695 | $this->is_subscription( $order_id ) && 696 | floatval( 0 ) === floatval( $data['amount_gross'] ) 697 | ) { 698 | if ( self::get_order_prop( $order, 'order_key' ) !== $order_key ) { 699 | $this->log( 'Order key does not match' ); 700 | exit; 701 | } 702 | 703 | $this->log( '- Change Payment Method' ); 704 | $status = strtolower( $data['payment_status'] ); 705 | if ( 'complete' === $status && isset( $data['token'] ) ) { 706 | $token = sanitize_text_field( $data['token'] ); 707 | $subscription = wcs_get_subscription( $order_id ); 708 | if ( ! empty( $subscription ) && ! empty( $token ) ) { 709 | $old_token = $this->_get_subscription_token( $subscription ); 710 | // Cancel old subscription token of subscription if we have it. 711 | if ( ! empty( $old_token ) ) { 712 | $this->cancel_subscription_listener( $subscription ); 713 | } 714 | 715 | // Set new subscription token on subscription. 716 | $this->_set_subscription_token( $token, $subscription ); 717 | $this->log( 'Payfast token updated on Subcription: ' . $order_id ); 718 | } 719 | } 720 | return; 721 | } 722 | 723 | // Check data against internal order. 724 | if ( ! $payfast_error && ! $payfast_done ) { 725 | $this->log( 'Check data against internal order' ); 726 | 727 | // alter order object to be the renewal order if 728 | // the ITN request comes as a result of a renewal submission request. 729 | $description = json_decode( $data['item_description'] ); 730 | 731 | if ( ! empty( $description->renewal_order_id ) ) { 732 | $renewal_order = wc_get_order( $description->renewal_order_id ); 733 | if ( ! empty( $renewal_order ) && function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $renewal_order ) ) { 734 | $order = $renewal_order; 735 | } 736 | } 737 | 738 | // Check order amount. 739 | if ( ! $this->amounts_equal( $data['amount_gross'], self::get_order_prop( $order, 'order_total' ) ) ) { 740 | $payfast_error = true; 741 | $payfast_error_message = PF_ERR_AMOUNT_MISMATCH; 742 | } elseif ( strcasecmp( $data['custom_str1'], self::get_order_prop( $original_order, 'order_key' ) ) !== 0 ) { 743 | // Check session ID. 744 | $payfast_error = true; 745 | $payfast_error_message = PF_ERR_SESSIONID_MISMATCH; 746 | } 747 | } 748 | 749 | // Get internal order and verify it hasn't already been processed. 750 | if ( ! $payfast_error && ! $payfast_done ) { 751 | $this->log_order_details( $order ); 752 | 753 | // Check if order has already been processed. 754 | if ( 'completed' === self::get_order_prop( $order, 'status' ) ) { 755 | $this->log( 'Order has already been processed' ); 756 | $payfast_done = true; 757 | } 758 | } 759 | 760 | // If an error occurred. 761 | if ( $payfast_error ) { 762 | $this->log( 'Error occurred: ' . $payfast_error_message ); 763 | 764 | if ( $this->send_debug_email ) { 765 | $this->log( 'Sending email notification' ); 766 | 767 | // Send an email. 768 | $subject = 'Payfast ITN error: ' . $payfast_error_message; 769 | $body = 770 | "Hi,\n\n" . 771 | "An invalid Payfast transaction on your website requires attention\n" . 772 | "------------------------------------------------------------\n" . 773 | 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" . 774 | 'Remote IP Address: ' . sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) . "\n" . 775 | 'Remote host name: ' . gethostbyaddr( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) . "\n" . 776 | 'Purchase ID: ' . self::get_order_prop( $order, 'id' ) . "\n" . 777 | 'User ID: ' . self::get_order_prop( $order, 'user_id' ) . "\n"; 778 | if ( isset( $data['pf_payment_id'] ) ) { 779 | $body .= 'Payfast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n"; 780 | } 781 | if ( isset( $data['payment_status'] ) ) { 782 | $body .= 'Payfast Payment Status: ' . esc_html( $data['payment_status'] ) . "\n"; 783 | } 784 | 785 | $body .= "\nError: " . $payfast_error_message . "\n"; 786 | 787 | switch ( $payfast_error_message ) { 788 | case PF_ERR_AMOUNT_MISMATCH: 789 | $body .= 790 | 'Value received : ' . esc_html( $data['amount_gross'] ) . "\n" 791 | . 'Value should be: ' . self::get_order_prop( $order, 'order_total' ); 792 | break; 793 | 794 | case PF_ERR_ORDER_ID_MISMATCH: 795 | $body .= 796 | 'Value received : ' . esc_html( $data['custom_str3'] ) . "\n" 797 | . 'Value should be: ' . self::get_order_prop( $order, 'id' ); 798 | break; 799 | 800 | case PF_ERR_SESSIONID_MISMATCH: 801 | $body .= 802 | 'Value received : ' . esc_html( $data['custom_str1'] ) . "\n" 803 | . 'Value should be: ' . self::get_order_prop( $order, 'id' ); 804 | break; 805 | 806 | // For all other errors there is no need to add additional information. 807 | default: 808 | break; 809 | } 810 | 811 | wp_mail( $debug_email, $subject, $body ); 812 | } // End if. 813 | } elseif ( ! $payfast_done ) { 814 | 815 | $this->log( 'Check status and update order' ); 816 | 817 | if ( self::get_order_prop( $original_order, 'order_key' ) !== $order_key ) { 818 | $this->log( 'Order key does not match' ); 819 | exit; 820 | } 821 | 822 | $status = strtolower( $data['payment_status'] ); 823 | 824 | $subscriptions = array(); 825 | if ( function_exists( 'wcs_get_subscriptions_for_renewal_order' ) && function_exists( 'wcs_get_subscriptions_for_order' ) ) { 826 | $subscriptions = array_merge( 827 | wcs_get_subscriptions_for_renewal_order( $order_id ), 828 | wcs_get_subscriptions_for_order( $order_id ) 829 | ); 830 | } 831 | 832 | if ( 'complete' !== $status && 'cancelled' !== $status ) { 833 | foreach ( $subscriptions as $subscription ) { 834 | $this->_set_renewal_flag( $subscription ); 835 | } 836 | } 837 | 838 | if ( 'complete' === $status ) { 839 | $this->handle_itn_payment_complete( $data, $order, $subscriptions ); 840 | } elseif ( 'failed' === $status ) { 841 | $this->handle_itn_payment_failed( $data, $order ); 842 | } elseif ( 'pending' === $status ) { 843 | $this->handle_itn_payment_pending( $data, $order ); 844 | } elseif ( 'cancelled' === $status ) { 845 | $this->handle_itn_payment_cancelled( $data, $order, $subscriptions ); 846 | } 847 | } // End if. 848 | 849 | $this->log( 850 | PHP_EOL 851 | . '----------' 852 | . PHP_EOL . 'End ITN call' 853 | . PHP_EOL . '----------' 854 | ); 855 | } 856 | 857 | /** 858 | * Handle logging the order details. 859 | * 860 | * @since 1.4.5 861 | * 862 | * @param WC_Order $order Order object. 863 | */ 864 | public function log_order_details( $order ) { 865 | $customer_id = $order->get_user_id(); 866 | 867 | $details = 'Order Details:' 868 | . PHP_EOL . 'customer id:' . $customer_id 869 | . PHP_EOL . 'order id: ' . $order->get_id() 870 | . PHP_EOL . 'parent id: ' . $order->get_parent_id() 871 | . PHP_EOL . 'status: ' . $order->get_status() 872 | . PHP_EOL . 'total: ' . $order->get_total() 873 | . PHP_EOL . 'currency: ' . $order->get_currency() 874 | . PHP_EOL . 'key: ' . $order->get_order_key() 875 | . ''; 876 | 877 | $this->log( $details ); 878 | } 879 | 880 | /** 881 | * This function mainly responds to ITN cancel requests initiated on Payfast, but also acts 882 | * just in case they are not cancelled. 883 | * 884 | * @version 1.4.3 Subscriptions flag 885 | * 886 | * @param array $data Should be from the Gateway ITN callback. 887 | * @param WC_Order $order Order object. 888 | * @param WC_Subscription[] $subscriptions Array of subscriptions. 889 | */ 890 | public function handle_itn_payment_cancelled( $data, $order, $subscriptions ) { 891 | 892 | remove_action( 'woocommerce_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); 893 | foreach ( $subscriptions as $subscription ) { 894 | if ( 'cancelled' !== $subscription->get_status() ) { 895 | $subscription->update_status( 'cancelled', esc_html__( 'Merchant cancelled subscription on Payfast.', 'woocommerce-gateway-payfast' ) ); 896 | $this->_delete_subscription_token( $subscription ); 897 | } 898 | } 899 | add_action( 'woocommerce_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); 900 | } 901 | 902 | /** 903 | * This function handles payment complete request by Payfast. 904 | * 905 | * @version 1.4.3 Subscriptions flag 906 | * 907 | * @param array $data Should be from the Gateway ITN callback. 908 | * @param WC_Order $order Order object. 909 | * @param WC_Subscription[] $subscriptions Array of subscriptions. 910 | */ 911 | public function handle_itn_payment_complete( $data, $order, $subscriptions ) { 912 | $this->log( '- Complete' ); 913 | $order->add_order_note( esc_html__( 'ITN payment completed', 'woocommerce-gateway-payfast' ) ); 914 | $order->update_meta_data( 'payfast_amount_fee', $data['amount_fee'] ); 915 | $order->update_meta_data( 'payfast_amount_net', $data['amount_net'] ); 916 | $order_id = self::get_order_prop( $order, 'id' ); 917 | 918 | // Store token for future subscription deductions. 919 | if ( count( $subscriptions ) > 0 && isset( $data['token'] ) ) { 920 | if ( $this->_has_renewal_flag( reset( $subscriptions ) ) ) { 921 | // Renewal flag is set to true, so we need to cancel previous token since we will create a new one. 922 | $this->log( 'Cancel previous subscriptions with token ' . $this->_get_subscription_token( reset( $subscriptions ) ) ); 923 | 924 | // Only request API cancel token for the first subscription since all of them are using the same token. 925 | $this->cancel_subscription_listener( reset( $subscriptions ) ); 926 | } 927 | 928 | $token = sanitize_text_field( $data['token'] ); 929 | foreach ( $subscriptions as $subscription ) { 930 | $this->_delete_renewal_flag( $subscription ); 931 | $this->_set_subscription_token( $token, $subscription ); 932 | } 933 | } 934 | 935 | // Mark payment as complete. 936 | $order->payment_complete( $data['pf_payment_id'] ); 937 | 938 | $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); 939 | $vendor_name = get_bloginfo( 'name', 'display' ); 940 | $vendor_url = home_url( '/' ); 941 | if ( $this->send_debug_email ) { 942 | $subject = 'Payfast ITN on your site'; 943 | $body = 944 | "Hi,\n\n" 945 | . "A Payfast transaction has been completed on your website\n" 946 | . "------------------------------------------------------------\n" 947 | . 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" 948 | . 'Purchase ID: ' . esc_html( $data['m_payment_id'] ) . "\n" 949 | . 'Payfast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n" 950 | . 'Payfast Payment Status: ' . esc_html( $data['payment_status'] ) . "\n" 951 | . 'Order Status Code: ' . self::get_order_prop( $order, 'status' ); 952 | wp_mail( $debug_email, $subject, $body ); 953 | } 954 | 955 | /** 956 | * Fires after handling the Payment Complete ITN from Payfast. 957 | * 958 | * @since 1.4.22 959 | * 960 | * @param array $data ITN Payload. 961 | * @param WC_Order $order Order Object. 962 | * @param WC_Subscription[] $subscriptions Subscription array. 963 | */ 964 | do_action( 'woocommerce_payfast_handle_itn_payment_complete', $data, $order, $subscriptions ); 965 | } 966 | 967 | /** 968 | * Handle payment failed request by Payfast. 969 | * 970 | * @param array $data Should be from the Gateway ITN callback. 971 | * @param WC_Order $order Order object. 972 | */ 973 | public function handle_itn_payment_failed( $data, $order ) { 974 | $this->log( '- Failed' ); 975 | /* translators: 1: payment status */ 976 | $order->update_status( 'failed', sprintf( __( 'Payment %s via ITN.', 'woocommerce-gateway-payfast' ), strtolower( sanitize_text_field( $data['payment_status'] ) ) ) ); 977 | $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); 978 | $vendor_name = get_bloginfo( 'name', 'display' ); 979 | $vendor_url = home_url( '/' ); 980 | 981 | if ( $this->send_debug_email ) { 982 | $subject = 'Payfast ITN Transaction on your site'; 983 | $body = 984 | "Hi,\n\n" . 985 | "A failed Payfast transaction on your website requires attention\n" . 986 | "------------------------------------------------------------\n" . 987 | 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" . 988 | 'Purchase ID: ' . self::get_order_prop( $order, 'id' ) . "\n" . 989 | 'User ID: ' . self::get_order_prop( $order, 'user_id' ) . "\n" . 990 | 'Payfast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n" . 991 | 'Payfast Payment Status: ' . esc_html( $data['payment_status'] ); 992 | wp_mail( $debug_email, $subject, $body ); 993 | } 994 | } 995 | 996 | /** 997 | * Handle payment pending request by Payfast. 998 | * 999 | * @since 1.4.0 1000 | * 1001 | * @param array $data Should be from the Gateway ITN callback. 1002 | * @param WC_Order $order Order object. 1003 | */ 1004 | public function handle_itn_payment_pending( $data, $order ) { 1005 | $this->log( '- Pending' ); 1006 | // Need to wait for "Completed" before processing. 1007 | /* translators: 1: payment status */ 1008 | $order->update_status( 'on-hold', sprintf( esc_html__( 'Payment %s via ITN.', 'woocommerce-gateway-payfast' ), strtolower( sanitize_text_field( $data['payment_status'] ) ) ) ); 1009 | } 1010 | 1011 | /** 1012 | * Get the pre-order fee. 1013 | * 1014 | * @param string $order_id Order ID. 1015 | * @return double 1016 | */ 1017 | public function get_pre_order_fee( $order_id ) { 1018 | foreach ( wc_get_order( $order_id )->get_fees() as $fee ) { 1019 | if ( is_array( $fee ) && 'Pre-Order Fee' === $fee['name'] ) { 1020 | return doubleval( $fee['line_total'] ) + doubleval( $fee['line_tax'] ); 1021 | } 1022 | } 1023 | } 1024 | 1025 | /** 1026 | * Whether order contains a pre-order. 1027 | * 1028 | * @param string $order_id Order ID. 1029 | * @return bool Whether order contains a pre-order. 1030 | */ 1031 | public function order_contains_pre_order( $order_id ) { 1032 | if ( class_exists( 'WC_Pre_Orders_Order' ) ) { 1033 | return WC_Pre_Orders_Order::order_contains_pre_order( $order_id ); 1034 | } 1035 | return false; 1036 | } 1037 | 1038 | /** 1039 | * Whether the order requires payment tokenization. 1040 | * 1041 | * @param string $order_id Order ID. 1042 | * @return bool Whether the order requires payment tokenization. 1043 | */ 1044 | public function order_requires_payment_tokenization( $order_id ) { 1045 | if ( class_exists( 'WC_Pre_Orders_Order' ) ) { 1046 | return WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ); 1047 | } 1048 | return false; 1049 | } 1050 | 1051 | /** 1052 | * Whether order contains a pre-order fee. 1053 | * 1054 | * @return bool Whether order contains a pre-order fee. 1055 | */ 1056 | public function cart_contains_pre_order_fee() { 1057 | if ( class_exists( 'WC_Pre_Orders_Cart' ) ) { 1058 | return WC_Pre_Orders_Cart::cart_contains_pre_order_fee(); 1059 | } 1060 | return false; 1061 | } 1062 | /** 1063 | * Store the Payfast subscription token 1064 | * 1065 | * @param string $token Payfast subscription token. 1066 | * @param WC_Subscription $subscription The subscription object. 1067 | */ 1068 | protected function _set_subscription_token( $token, $subscription ) { 1069 | $subscription->update_meta_data( '_payfast_subscription_token', $token ); 1070 | $subscription->save_meta_data(); 1071 | } 1072 | 1073 | /** 1074 | * Retrieve the Payfast subscription token for a given order id. 1075 | * 1076 | * @param WC_Subscription $subscription The subscription object. 1077 | * @return mixed Payfast subscription token. 1078 | */ 1079 | protected function _get_subscription_token( $subscription ) { 1080 | return $subscription->get_meta( '_payfast_subscription_token', true ); 1081 | } 1082 | 1083 | /** 1084 | * Retrieve the Payfast subscription token for a given order id. 1085 | * 1086 | * @param WC_Subscription $subscription The subscription object. 1087 | * @return mixed 1088 | */ 1089 | protected function _delete_subscription_token( $subscription ) { 1090 | return $subscription->delete_meta_data( '_payfast_subscription_token' ); 1091 | } 1092 | 1093 | /** 1094 | * Store the Payfast renewal flag 1095 | * 1096 | * @since 1.4.3 1097 | * 1098 | * @param WC_Subscription $subscription The subscription object. 1099 | */ 1100 | protected function _set_renewal_flag( $subscription ) { 1101 | $subscription->update_meta_data( '_payfast_renewal_flag', 'true' ); 1102 | $subscription->save_meta_data(); 1103 | } 1104 | 1105 | /** 1106 | * Retrieve the Payfast renewal flag for a given order id. 1107 | * 1108 | * @since 1.4.3 1109 | * 1110 | * @param WC_Subscription $subscription The subscription object. 1111 | * @return bool 1112 | */ 1113 | protected function _has_renewal_flag( $subscription ) { 1114 | return 'true' === $subscription->get_meta( '_payfast_renewal_flag', true ); 1115 | } 1116 | 1117 | /** 1118 | * Retrieve the Payfast renewal flag for a given order id. 1119 | * 1120 | * @since 1.4.3 1121 | * 1122 | * @param WC_Subscription $subscription The subscription object. 1123 | */ 1124 | protected function _delete_renewal_flag( $subscription ) { 1125 | $subscription->delete_meta_data( '_payfast_renewal_flag' ); 1126 | $subscription->save_meta_data(); 1127 | } 1128 | 1129 | /** 1130 | * Wrapper for WooCommerce subscription function wc_is_subscription. 1131 | * 1132 | * @param WC_Order|int $order The order. 1133 | * @return bool 1134 | */ 1135 | public function is_subscription( $order ) { 1136 | if ( ! function_exists( 'wcs_is_subscription' ) ) { 1137 | return false; 1138 | } 1139 | return wcs_is_subscription( $order ); 1140 | } 1141 | 1142 | /** 1143 | * Cancel Payfast Tokenization(ad-hoc) token if subscription changed to other payment method. 1144 | * 1145 | * @param WC_Subscription $subscription The subscription for which the payment method changed. 1146 | * @param string $new_payment_method New payment method name. 1147 | */ 1148 | public function maybe_cancel_subscription_token( $subscription, $new_payment_method ) { 1149 | $token = $this->_get_subscription_token( $subscription ); 1150 | if ( empty( $token ) || $this->id === $new_payment_method ) { 1151 | return; 1152 | } 1153 | $this->cancel_subscription_listener( $subscription ); 1154 | $this->_delete_subscription_token( $subscription ); 1155 | 1156 | $this->log( 'Payfast subscription token Cancelled.' ); 1157 | } 1158 | 1159 | /** 1160 | * Whether order contains a subscription. 1161 | * 1162 | * Wrapper function for wcs_order_contains_subscription 1163 | * 1164 | * @param WC_Order $order Order object. 1165 | * @return bool Whether order contains a subscription. 1166 | */ 1167 | public function order_contains_subscription( $order ) { 1168 | if ( ! function_exists( 'wcs_order_contains_subscription' ) ) { 1169 | return false; 1170 | } 1171 | return wcs_order_contains_subscription( $order ); 1172 | } 1173 | 1174 | /** 1175 | * Process scheduled subscription payment and update the subscription status accordingly. 1176 | * 1177 | * @param float $amount_to_charge Subscription cost. 1178 | * @param WC_Order $renewal_order Renewal order object. 1179 | */ 1180 | public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) { 1181 | 1182 | $subscription = wcs_get_subscription( $renewal_order->get_meta( '_subscription_renewal', true ) ); 1183 | $this->log( 'Attempting to renew subscription from renewal order ' . self::get_order_prop( $renewal_order, 'id' ) ); 1184 | 1185 | if ( empty( $subscription ) ) { 1186 | $this->log( 'Subscription from renewal order was not found.' ); 1187 | return; 1188 | } 1189 | 1190 | $response = $this->submit_subscription_payment( $subscription, $amount_to_charge ); 1191 | 1192 | if ( is_wp_error( $response ) ) { 1193 | /* translators: 1: error code 2: error message */ 1194 | $renewal_order->update_status( 'failed', sprintf( esc_html__( 'Payfast Subscription renewal transaction failed (%1$s:%2$s)', 'woocommerce-gateway-payfast' ), $response->get_error_code(), $response->get_error_message() ) ); 1195 | } 1196 | // Payment will be completion will be capture only when the ITN callback is sent to $this->handle_itn_request(). 1197 | $renewal_order->add_order_note( esc_html__( 'Payfast Subscription renewal transaction submitted.', 'woocommerce-gateway-payfast' ) ); 1198 | } 1199 | 1200 | /** 1201 | * Attempt to process a subscription payment on the Payfast gateway. 1202 | * 1203 | * @param WC_Subscription $subscription The subscription object. 1204 | * @param float $amount_to_charge The amount to charge. 1205 | * @return mixed WP_Error on failure, bool true on success 1206 | */ 1207 | public function submit_subscription_payment( $subscription, $amount_to_charge ) { 1208 | $token = $this->_get_subscription_token( $subscription ); 1209 | $item_name = $this->get_subscription_name( $subscription ); 1210 | 1211 | foreach ( $subscription->get_related_orders( 'all', 'renewal' ) as $order ) { 1212 | $statuses_to_charge = array( 'on-hold', 'failed', 'pending' ); 1213 | if ( in_array( $order->get_status(), $statuses_to_charge, true ) ) { 1214 | $latest_order_to_renew = $order; 1215 | break; 1216 | } 1217 | } 1218 | $item_description = wp_json_encode( array( 'renewal_order_id' => self::get_order_prop( $latest_order_to_renew, 'id' ) ) ); 1219 | 1220 | return $this->submit_ad_hoc_payment( $token, $amount_to_charge, $item_name, $item_description ); 1221 | } 1222 | 1223 | /** 1224 | * Get a name for the subscription item. For multiple 1225 | * item only Subscription $date will be returned. 1226 | * 1227 | * For subscriptions with no items Site/Blog name will be returned. 1228 | * 1229 | * @param WC_Subscription $subscription The subscription object. 1230 | * @return string 1231 | */ 1232 | public function get_subscription_name( $subscription ) { 1233 | 1234 | if ( $subscription->get_item_count() > 1 ) { 1235 | return $subscription->get_date_to_display( 'start' ); 1236 | } else { 1237 | $items = $subscription->get_items(); 1238 | 1239 | if ( empty( $items ) ) { 1240 | return get_bloginfo( 'name' ); 1241 | } 1242 | 1243 | $item = array_shift( $items ); 1244 | return $item['name']; 1245 | } 1246 | } 1247 | 1248 | /** 1249 | * Setup api data for the the adhoc payment. 1250 | * 1251 | * @since 1.4.0 introduced. 1252 | * 1253 | * @param string $token Payfast subscription token. 1254 | * @param float $amount_to_charge Amount to charge. 1255 | * @param string $item_name Item name. 1256 | * @param string $item_description Item description. 1257 | * 1258 | * @return bool|WP_Error WP_Error on failure, bool true on success 1259 | */ 1260 | public function submit_ad_hoc_payment( $token, $amount_to_charge, $item_name, $item_description ) { 1261 | $args = array( 1262 | 'body' => array( 1263 | 'amount' => $amount_to_charge * 100, // Convert to cents. 1264 | 'item_name' => $item_name, 1265 | 'item_description' => $item_description, 1266 | ), 1267 | ); 1268 | return $this->api_request( 'adhoc', $token, $args ); 1269 | } 1270 | 1271 | /** 1272 | * Send off API request. 1273 | * 1274 | * @since 1.4.0 introduced. 1275 | * 1276 | * @param string $command API command. 1277 | * @param string $token Payfast subscription token. 1278 | * @param array $api_args Arguments for the API request. See WP documentation for wp_remote_request. 1279 | * @param string $method GET | PUT | POST | DELETE. 1280 | * 1281 | * @return bool|WP_Error WP_Error on failure, bool true on success 1282 | */ 1283 | public function api_request( $command, $token, $api_args, $method = 'POST' ) { 1284 | if ( empty( $token ) ) { 1285 | $this->log( 'Error posting API request: No token supplied', true ); 1286 | return new WP_Error( '404', esc_html__( 'Can not submit Payfast request with an empty token', 'woocommerce-gateway-payfast' ), $results ); 1287 | } 1288 | 1289 | $api_endpoint = "https://api.payfast.co.za/subscriptions/$token/$command"; 1290 | $api_endpoint .= 'yes' === $this->get_option( 'testmode' ) ? '?testing=true' : ''; 1291 | 1292 | $timestamp = current_time( rtrim( DateTime::ATOM, 'P' ) ) . '+02:00'; 1293 | $api_args['timeout'] = 45; 1294 | $api_args['headers'] = array( 1295 | 'merchant-id' => $this->merchant_id, 1296 | 'timestamp' => $timestamp, 1297 | 'version' => 'v1', 1298 | ); 1299 | 1300 | // Set content length to fix "411: requests require a Content-length header" error. 1301 | if ( 'cancel' === $command && ! isset( $api_args['body'] ) ) { 1302 | $api_args['headers']['content-length'] = 0; 1303 | } 1304 | 1305 | // Generate signature. 1306 | $all_api_variables = array_merge( $api_args['headers'], (array) $api_args['body'] ); 1307 | $api_args['headers']['signature'] = md5( $this->_generate_parameter_string( $all_api_variables ) ); 1308 | $api_args['method'] = strtoupper( $method ); 1309 | 1310 | $results = wp_remote_request( $api_endpoint, $api_args ); 1311 | 1312 | if ( is_wp_error( $results ) ) { 1313 | return $results; 1314 | } 1315 | 1316 | // Check Payfast server response. 1317 | if ( 200 !== $results['response']['code'] ) { 1318 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1319 | $this->log( "Error posting API request:\n" . print_r( $results['response'], true ) ); 1320 | return new WP_Error( $results['response']['code'], json_decode( $results['body'] )->data->response, $results ); 1321 | } 1322 | 1323 | // Check adhoc bank charge response. 1324 | $results_data = json_decode( $results['body'], true )['data']; 1325 | 1326 | // Sandbox ENV returns true(boolean) in response, while Production ENV "true"(string) in response. 1327 | if ( 'adhoc' === $command && ! ( 'true' === $results_data['response'] || true === $results_data['response'] ) ) { 1328 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1329 | $this->log( "Error posting API request:\n" . print_r( $results_data, true ) ); 1330 | 1331 | $code = is_array( $results_data['response'] ) ? $results_data['response']['code'] : $results_data['response']; 1332 | $message = is_array( $results_data['response'] ) ? $results_data['response']['reason'] : $results_data['message']; 1333 | // Use trim here to display it properly e.g. on an order note, since Payfast can include CRLF in a message. 1334 | return new WP_Error( $code, trim( $message ), $results ); 1335 | } 1336 | 1337 | $maybe_json = json_decode( $results['body'], true ); 1338 | 1339 | if ( ! is_null( $maybe_json ) && isset( $maybe_json['status'] ) && 'failed' === $maybe_json['status'] ) { 1340 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1341 | $this->log( "Error posting API request:\n" . print_r( $results['body'], true ) ); 1342 | 1343 | // Use trim here to display it properly e.g. on an order note, since Payfast can include CRLF in a message. 1344 | return new WP_Error( $maybe_json['code'], trim( $maybe_json['data']['message'] ), $results['body'] ); 1345 | } 1346 | 1347 | return true; 1348 | } 1349 | 1350 | /** 1351 | * Responds to Subscriptions extension cancellation event. 1352 | * 1353 | * @since 1.4.0 introduced. 1354 | * @param WC_Subscription $subscription The subscription object. 1355 | */ 1356 | public function cancel_subscription_listener( $subscription ) { 1357 | $token = $this->_get_subscription_token( $subscription ); 1358 | if ( empty( $token ) ) { 1359 | return; 1360 | } 1361 | $this->api_request( 'cancel', $token, array(), 'PUT' ); 1362 | } 1363 | 1364 | /** 1365 | * Cancel a pre-order subscription. 1366 | * 1367 | * @since 1.4.0 1368 | * 1369 | * @param string $token Payfast subscription token. 1370 | * 1371 | * @return bool|WP_Error WP_Error on failure, bool true on success. 1372 | */ 1373 | public function cancel_pre_order_subscription( $token ) { 1374 | return $this->api_request( 'cancel', $token, array(), 'PUT' ); 1375 | } 1376 | 1377 | /** 1378 | * Generate the parameter string to send to Payfast. 1379 | * 1380 | * @since 1.4.0 introduced. 1381 | * 1382 | * @param array $api_data Data to send to the Payfast API. 1383 | * @param bool $sort_data_before_merge Whether to sort before merge. Default true. 1384 | * @param bool $skip_empty_values Should key value pairs be ignored when generating signature? Default true. 1385 | * 1386 | * @return string 1387 | */ 1388 | protected function _generate_parameter_string( $api_data, $sort_data_before_merge = true, $skip_empty_values = true ) { 1389 | 1390 | // if sorting is required the passphrase should be added in before sort. 1391 | if ( ! empty( $this->pass_phrase ) && $sort_data_before_merge ) { 1392 | $api_data['passphrase'] = $this->pass_phrase; 1393 | } 1394 | 1395 | if ( $sort_data_before_merge ) { 1396 | ksort( $api_data ); 1397 | } 1398 | 1399 | // concatenate the array key value pairs. 1400 | $parameter_string = ''; 1401 | foreach ( $api_data as $key => $val ) { 1402 | 1403 | if ( $skip_empty_values && empty( $val ) ) { 1404 | continue; 1405 | } 1406 | 1407 | if ( 'signature' !== $key ) { 1408 | // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode -- legacy code, validation required prior to switching to rawurlencode. 1409 | $val = urlencode( $val ); 1410 | $parameter_string .= "$key=$val&"; 1411 | } 1412 | } 1413 | // When not sorting passphrase should be added to the end before md5. 1414 | if ( $sort_data_before_merge ) { 1415 | $parameter_string = rtrim( $parameter_string, '&' ); 1416 | } elseif ( ! empty( $this->pass_phrase ) ) { 1417 | // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode -- legacy code, validation required prior to switching to rawurlencode. 1418 | $parameter_string .= 'passphrase=' . urlencode( $this->pass_phrase ); 1419 | } else { 1420 | $parameter_string = rtrim( $parameter_string, '&' ); 1421 | } 1422 | 1423 | return $parameter_string; 1424 | } 1425 | 1426 | /** 1427 | * Process pre-order payment. 1428 | * 1429 | * @since 1.4.0 introduced. 1430 | * 1431 | * @param WC_Order $order Order object. 1432 | */ 1433 | public function process_pre_order_payments( $order ) { 1434 | wc_deprecated_function( 'process_pre_order_payments', '1.6.3' ); 1435 | } 1436 | 1437 | /** 1438 | * Setup constants. 1439 | * 1440 | * Setup common values and messages used by the Payfast gateway. 1441 | * 1442 | * @since 1.0.0 1443 | */ 1444 | public function setup_constants() { 1445 | // Create user agent string. 1446 | define( 'PF_SOFTWARE_NAME', 'WooCommerce' ); 1447 | define( 'PF_SOFTWARE_VER', WC_VERSION ); 1448 | define( 'PF_MODULE_NAME', 'WooCommerce-Payfast-Free' ); 1449 | define( 'PF_MODULE_VER', $this->version ); 1450 | 1451 | // Features 1452 | // - PHP. 1453 | $pf_features = 'PHP ' . phpversion() . ';'; 1454 | 1455 | // - cURL. 1456 | if ( in_array( 'curl', get_loaded_extensions(), true ) ) { 1457 | define( 'PF_CURL', '' ); 1458 | $pf_version = curl_version(); 1459 | $pf_features .= ' curl ' . $pf_version['version'] . ';'; 1460 | } else { 1461 | $pf_features .= ' nocurl;'; 1462 | } 1463 | 1464 | // Create user agent. 1465 | define( 'PF_USER_AGENT', PF_SOFTWARE_NAME . '/' . PF_SOFTWARE_VER . ' (' . trim( $pf_features ) . ') ' . PF_MODULE_NAME . '/' . PF_MODULE_VER ); 1466 | 1467 | // General Defines. 1468 | define( 'PF_TIMEOUT', 15 ); 1469 | define( 'PF_EPSILON', 0.01 ); 1470 | 1471 | // Error messages. 1472 | define( 'PF_ERR_AMOUNT_MISMATCH', esc_html__( 'Amount mismatch', 'woocommerce-gateway-payfast' ) ); 1473 | define( 'PF_ERR_BAD_ACCESS', esc_html__( 'Bad access of page', 'woocommerce-gateway-payfast' ) ); 1474 | define( 'PF_ERR_BAD_SOURCE_IP', esc_html__( 'Bad source IP address', 'woocommerce-gateway-payfast' ) ); 1475 | define( 'PF_ERR_CONNECT_FAILED', esc_html__( 'Failed to connect to Payfast', 'woocommerce-gateway-payfast' ) ); 1476 | define( 'PF_ERR_INVALID_SIGNATURE', esc_html__( 'Security signature mismatch', 'woocommerce-gateway-payfast' ) ); 1477 | define( 'PF_ERR_MERCHANT_ID_MISMATCH', esc_html__( 'Merchant ID mismatch', 'woocommerce-gateway-payfast' ) ); 1478 | define( 'PF_ERR_NO_SESSION', esc_html__( 'No saved session found for ITN transaction', 'woocommerce-gateway-payfast' ) ); 1479 | define( 'PF_ERR_ORDER_ID_MISSING_URL', esc_html__( 'Order ID not present in URL', 'woocommerce-gateway-payfast' ) ); 1480 | define( 'PF_ERR_ORDER_ID_MISMATCH', esc_html__( 'Order ID mismatch', 'woocommerce-gateway-payfast' ) ); 1481 | define( 'PF_ERR_ORDER_INVALID', esc_html__( 'This order ID is invalid', 'woocommerce-gateway-payfast' ) ); 1482 | define( 'PF_ERR_ORDER_NUMBER_MISMATCH', esc_html__( 'Order Number mismatch', 'woocommerce-gateway-payfast' ) ); 1483 | define( 'PF_ERR_ORDER_PROCESSED', esc_html__( 'This order has already been processed', 'woocommerce-gateway-payfast' ) ); 1484 | define( 'PF_ERR_PDT_FAIL', esc_html__( 'PDT query failed', 'woocommerce-gateway-payfast' ) ); 1485 | define( 'PF_ERR_PDT_TOKEN_MISSING', esc_html__( 'PDT token not present in URL', 'woocommerce-gateway-payfast' ) ); 1486 | define( 'PF_ERR_SESSIONID_MISMATCH', esc_html__( 'Session ID mismatch', 'woocommerce-gateway-payfast' ) ); 1487 | define( 'PF_ERR_UNKNOWN', esc_html__( 'Unkown error occurred', 'woocommerce-gateway-payfast' ) ); 1488 | 1489 | // General messages. 1490 | define( 'PF_MSG_OK', esc_html__( 'Payment was successful', 'woocommerce-gateway-payfast' ) ); 1491 | define( 'PF_MSG_FAILED', esc_html__( 'Payment has failed', 'woocommerce-gateway-payfast' ) ); 1492 | define( 'PF_MSG_PENDING', esc_html__( 'The payment is pending. Please note, you will receive another Instant Transaction Notification when the payment status changes to "Completed", or "Failed"', 'woocommerce-gateway-payfast' ) ); 1493 | 1494 | /** 1495 | * Fires after Payfast constants are setup. 1496 | * 1497 | * @since 1.4.13 1498 | */ 1499 | do_action( 'woocommerce_gateway_payfast_setup_constants' ); 1500 | } 1501 | 1502 | /** 1503 | * Log system processes. 1504 | * 1505 | * @since 1.0.0 1506 | * 1507 | * @param string $message Log message. 1508 | */ 1509 | public function log( $message ) { 1510 | if ( 'yes' === $this->get_option( 'testmode' ) || $this->enable_logging ) { 1511 | if ( empty( $this->logger ) ) { 1512 | $this->logger = new WC_Logger(); 1513 | } 1514 | $this->logger->add( 'payfast', $message ); 1515 | } 1516 | } 1517 | 1518 | /** 1519 | * Validate the signature against the returned data. 1520 | * 1521 | * @since 1.0.0 1522 | * 1523 | * @param array $data Returned data. 1524 | * @param string $signature Signature to check. 1525 | * @return bool Whether the signature is valid. 1526 | */ 1527 | public function validate_signature( $data, $signature ) { 1528 | $result = $data['signature'] === $signature; 1529 | $this->log( 'Signature = ' . ( $result ? 'valid' : 'invalid' ) ); 1530 | return $result; 1531 | } 1532 | 1533 | /** 1534 | * Validate the IP address to make sure it's coming from Payfast. 1535 | * 1536 | * @param string $source_ip Source IP. 1537 | * @since 1.0.0 1538 | * @return bool 1539 | */ 1540 | public function is_valid_ip( $source_ip ) { 1541 | // Variable initialization. 1542 | $valid_hosts = array( 1543 | 'www.payfast.co.za', 1544 | 'sandbox.payfast.co.za', 1545 | 'w1w.payfast.co.za', 1546 | 'w2w.payfast.co.za', 1547 | ); 1548 | 1549 | $valid_ips = array(); 1550 | 1551 | foreach ( $valid_hosts as $pf_hostname ) { 1552 | $ips = gethostbynamel( $pf_hostname ); 1553 | 1554 | if ( false !== $ips ) { 1555 | $valid_ips = array_merge( $valid_ips, $ips ); 1556 | } 1557 | } 1558 | 1559 | // Remove duplicates. 1560 | $valid_ips = array_unique( $valid_ips ); 1561 | 1562 | // Adds support for X_Forwarded_For. 1563 | if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { 1564 | $x_forwarded_http_header = trim( current( preg_split( '/[,:]/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ); 1565 | $source_ip = rest_is_ip_address( $x_forwarded_http_header ) ? rest_is_ip_address( $x_forwarded_http_header ) : $source_ip; 1566 | } 1567 | 1568 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1569 | $this->log( "Valid IPs:\n" . print_r( $valid_ips, true ) ); 1570 | $is_valid_ip = in_array( $source_ip, $valid_ips, true ); 1571 | 1572 | /** 1573 | * Filter whether Payfast Gateway IP address is valid. 1574 | * 1575 | * @since 1.4.13 1576 | * 1577 | * @param bool $is_valid_ip Whether IP address is valid. 1578 | * @param bool $source_ip Source IP. 1579 | */ 1580 | return apply_filters( 'woocommerce_gateway_payfast_is_valid_ip', $is_valid_ip, $source_ip ); 1581 | } 1582 | 1583 | /** 1584 | * Validate response data. 1585 | * 1586 | * @since 1.0.0 1587 | * 1588 | * @param array $post_data POST data for original request. 1589 | * @param string $proxy Address of proxy to use or NULL if no proxy. 1590 | * @return bool 1591 | */ 1592 | public function validate_response_data( $post_data, $proxy = null ) { 1593 | $this->log( 'Host = ' . $this->validate_url ); 1594 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1595 | $this->log( 'Params = ' . print_r( $post_data, true ) ); 1596 | 1597 | if ( ! is_array( $post_data ) ) { 1598 | return false; 1599 | } 1600 | 1601 | $response = wp_remote_post( 1602 | $this->validate_url, 1603 | array( 1604 | 'body' => $post_data, 1605 | 'timeout' => 70, 1606 | 'user-agent' => PF_USER_AGENT, 1607 | ) 1608 | ); 1609 | 1610 | if ( is_wp_error( $response ) || empty( $response['body'] ) ) { 1611 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1612 | $this->log( "Response error:\n" . print_r( $response, true ) ); 1613 | return false; 1614 | } 1615 | 1616 | parse_str( $response['body'], $parsed_response ); 1617 | 1618 | $response = $parsed_response; 1619 | 1620 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for logging. 1621 | $this->log( "Response:\n" . print_r( $response, true ) ); 1622 | 1623 | // Interpret Response. 1624 | if ( is_array( $response ) && in_array( 'VALID', array_keys( $response ), true ) ) { 1625 | return true; 1626 | } else { 1627 | return false; 1628 | } 1629 | } 1630 | 1631 | /** 1632 | * Check the given amounts are equal. 1633 | * 1634 | * Checks to see whether the given amounts are equal using a proper floating 1635 | * point comparison with an Epsilon which ensures that insignificant decimal 1636 | * places are ignored in the comparison. 1637 | * 1638 | * eg. 100.00 is equal to 100.0001 1639 | * 1640 | * @since 1.0.0 1641 | * 1642 | * @param float $amount1 1st amount for comparison. 1643 | * @param float $amount2 2nd amount for comparison. 1644 | * 1645 | * @return bool 1646 | */ 1647 | public function amounts_equal( $amount1, $amount2 ) { 1648 | return ! ( abs( floatval( $amount1 ) - floatval( $amount2 ) ) > PF_EPSILON ); 1649 | } 1650 | 1651 | /** 1652 | * Get order property with compatibility check on order getter introduced 1653 | * in WC 3.0. 1654 | * 1655 | * @since 1.4.1 1656 | * 1657 | * @param WC_Order $order Order object. 1658 | * @param string $prop Property name. 1659 | * 1660 | * @return mixed Property value 1661 | */ 1662 | public static function get_order_prop( $order, $prop ) { 1663 | switch ( $prop ) { 1664 | case 'order_total': 1665 | $getter = array( $order, 'get_total' ); 1666 | break; 1667 | default: 1668 | $getter = array( $order, 'get_' . $prop ); 1669 | break; 1670 | } 1671 | 1672 | return is_callable( $getter ) ? call_user_func( $getter ) : $order->{ $prop }; 1673 | } 1674 | 1675 | /** 1676 | * Gets user-friendly error message strings from keys 1677 | * 1678 | * @param string $key The key representing an error. 1679 | * 1680 | * @return string The user-friendly error message for display 1681 | */ 1682 | public function get_error_message( $key ) { 1683 | switch ( $key ) { 1684 | case 'wc-gateway-payfast-error-invalid-currency': 1685 | return esc_html__( 'Your store uses a currency that Payfast doesn\'t support yet.', 'woocommerce-gateway-payfast' ); 1686 | case 'wc-gateway-payfast-error-missing-merchant-id': 1687 | return esc_html__( 'You forgot to fill your merchant ID.', 'woocommerce-gateway-payfast' ); 1688 | case 'wc-gateway-payfast-error-missing-merchant-key': 1689 | return esc_html__( 'You forgot to fill your merchant key.', 'woocommerce-gateway-payfast' ); 1690 | case 'wc-gateway-payfast-error-missing-pass-phrase': 1691 | return esc_html__( 'Payfast requires a passphrase to work.', 'woocommerce-gateway-payfast' ); 1692 | case 'wc-gateway-payfast-error-invalid-credentials': 1693 | return esc_html__( 'Invalid Payfast credentials. Please verify and enter the correct details.', 'woocommerce-gateway-payfast' ); 1694 | default: 1695 | return ''; 1696 | } 1697 | } 1698 | 1699 | /** 1700 | * Show possible admin notices 1701 | */ 1702 | public function admin_notices() { 1703 | 1704 | // Get requirement errors. 1705 | $errors_to_show = $this->check_requirements(); 1706 | 1707 | // If everything is in place, don't display it. 1708 | if ( ! count( $errors_to_show ) ) { 1709 | return; 1710 | } 1711 | 1712 | // If the gateway isn't enabled, don't show it. 1713 | if ( 'no' === $this->enabled ) { 1714 | return; 1715 | } 1716 | 1717 | // Use transients to display the admin notice once after saving values. 1718 | if ( ! get_transient( 'wc-gateway-payfast-admin-notice-transient' ) ) { 1719 | set_transient( 'wc-gateway-payfast-admin-notice-transient', 1, 1 ); 1720 | 1721 | echo '' 1722 | . esc_html__( 'To use Payfast as a payment provider, you need to fix the problems below:', 'woocommerce-gateway-payfast' ) . '
' 1723 | . ''; 140 | printf( 141 | /* translators: %s WooCommerce download URL link. */ 142 | esc_html__( 'WooCommerce Payfast Gateway requires WooCommerce to be installed and active. You can download %s here.', 'woocommerce-gateway-payfast' ), 143 | 'WooCommerce' 144 | ); 145 | echo '