Changeset 1573562
- Timestamp:
- 01/12/2017 08:22:48 PM (9 years ago)
- Location:
- woocommerce-payment-gateway/trunk
- Files:
-
- 2 edited
-
gateway-inspire.php (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
woocommerce-payment-gateway/trunk/gateway-inspire.php
r1100709 r1573562 1 1 <?php 2 3 2 /** 4 3 * Plugin Name: WooCommerce Payment Gateway - Inspire 5 4 * Plugin URI: http://www.inspirecommerce.com/woocommerce/ 6 5 * Description: Accept all major credit cards directly on your WooCommerce site in a seamless and secure checkout environment with Inspire Commerce. 7 * Version: 1.7.66 * Version: 2.0.0 8 7 * Author: innerfire 9 8 * Author URI: http://www.inspirecommerce.com/ 10 9 * License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 11 * 10 * Text Domain: woocommerce-gateway-inspire 11 * 12 12 * @package WordPress 13 13 * @author innerfire … … 15 15 */ 16 16 17 add_action( 'plugins_loaded', 'woocommerce_inspire_commerce_init', 0 );18 17 19 function woocommerce_inspire_commerce_init() {20 18 21 if ( ! class_exists( 'WC_Payment_Gateway' ) ) { 22 return; 23 }; 19 /** 20 * Inspire Commerce Class 21 */ 22 class WC_Inspire { 24 23 25 DEFINE ('PLUGIN_DIR', plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) . '/' );26 DEFINE ('GATEWAY_URL', 'https://secure.inspiregateway.net/api/transact.php');27 DEFINE ('QUERY_URL', 'https://secure.inspiregateway.net/api/query.php');28 24 29 25 /** 30 * Inspire Commerce Gateway Class26 * Constructor 31 27 */ 32 class WC_Inspire extends WC_Payment_Gateway { 28 public function __construct(){ 29 define( 'WC_INSPIRE_VERSION', '2.0.0' ); 30 define( 'WC_INSPIRE_TEMPLATE_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) . '/templates/' ); 31 define( 'WC_INSPIRE_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) ); 32 define( 'WC_INSPIRE_PLUGIN_DIR', plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) . '/' ); 33 define( 'WC_INSPIRE_MAIN_FILE', __FILE__ ); 34 define( 'GATEWAY_URL', 'https://secure.inspiregateway.net/api/transact.php'); 35 define( 'QUERY_URL', 'https://secure.inspiregateway.net/api/query.php'); 33 36 34 function __construct() { 35 36 // Register plugin information 37 $this->id = 'inspire'; 38 $this->has_fields = true; 39 $this->supports = array( 40 'products', 41 'subscriptions', 42 'subscription_cancellation', 43 'subscription_suspension', 44 'subscription_reactivation', 45 'subscription_amount_changes', 46 'subscription_date_changes', 47 'subscription_payment_method_change', 48 'refunds' 49 ); 50 51 // Create plugin fields and settings 52 $this->init_form_fields(); 53 $this->init_settings(); 54 55 // Get setting values 56 foreach ( $this->settings as $key => $val ) $this->$key = $val; 57 58 // Load plugin checkout icon 59 $this->icon = PLUGIN_DIR . 'images/cards.png'; 60 61 // Add hooks 62 add_action( 'admin_notices', array( $this, 'inspire_commerce_ssl_check' ) ); 63 add_action( 'woocommerce_before_my_account', array( $this, 'add_payment_method_options' ) ); 64 add_action( 'woocommerce_receipt_inspire', array( $this, 'receipt_page' ) ); 65 add_action( 'woocommerce_update_options_payment_gateways', array( $this, 'process_admin_options' ) ); 66 add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); 67 add_action( 'wp_enqueue_scripts', array( $this, 'add_inspire_scripts' ) ); 68 add_action( 'scheduled_subscription_payment_inspire', array( $this, 'process_scheduled_subscription_payment'), 0, 3 ); 69 70 } 71 72 /** 73 * Process a refund if supported 74 * @param int $order_id 75 * @param float $amount 76 * @param string $reason 77 * @return bool|wp_error True or false based on success, or a WP_Error object 78 */ 79 public function process_refund( $order_id, $amount = null, $reason = '' ) { 80 $order = wc_get_order( $order_id ); 81 82 $transaction_id = null; 83 84 $args = array( 85 'post_id' => $order->id, 86 'approve' => 'approve', 87 'type' => '' 88 ); 89 90 remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); 91 92 $comments = get_comments( $args ); 93 94 foreach ( $comments as $comment ) { 95 if (strpos($comment->comment_content, 'Transaction ID: ') !== false) { 96 $exploded_comment = explode(": ", $comment->comment_content); 97 $transaction_id = $exploded_comment[1]; 98 } 99 } 100 101 add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); 102 103 if ( ! $order || ! $transaction_id ) { 104 return false; 105 } 106 107 // Add transaction-specific details to the request 108 $transaction_details = array ( 109 'username' => $this->username, 110 'password' => $this->password, 111 'type' => 'refund', 112 'transactionid' => $transaction_id, 113 'ipaddress' => $_SERVER['REMOTE_ADDR'], 114 ); 115 116 if ( ! is_null( $amount ) ) { 117 $transaction_details['amount'] = number_format( $amount, 2, '.', '' ); 118 } 119 120 // Send request and get response from server 121 $response = $this->post_and_get_response( $transaction_details ); 122 123 // Check response 124 if ( $response['response'] == 1 ) { 125 // Success 126 $order->add_order_note( __( 'Inspire Commerce refund completed. Refund Transaction ID: ' , 'woocommerce' ) . $response['transactionid'] ); 127 return true; 128 } else { 129 // Failure 130 $order->add_order_note( __( 'Inspire Commerce refund error. Response data: ' , 'woocommerce' ) . http_build_query($response)); 131 return false; 132 } 133 } 134 135 136 /** 137 * Check if SSL is enabled and notify the user. 138 */ 139 function inspire_commerce_ssl_check() { 140 if ( get_option( 'woocommerce_force_ssl_checkout' ) == 'no' && $this->enabled == 'yes' ) { 141 echo '<div class="error"><p>' . sprintf( __('Inspire Commerce is enabled and the <a href="%s">force SSL option</a> is disabled; your checkout is not secure! Please enable SSL and ensure your server has a valid SSL certificate.', 'woothemes' ), admin_url( 'admin.php?page=woocommerce' ) ) . '</p></div>'; 142 } 143 } 144 145 /** 146 * Initialize Gateway Settings Form Fields. 147 */ 148 function init_form_fields() { 149 150 $this->form_fields = array( 151 'enabled' => array( 152 'title' => __( 'Enable/Disable', 'woothemes' ), 153 'label' => __( 'Enable Inspire Commerce', 'woothemes' ), 154 'type' => 'checkbox', 155 'description' => '', 156 'default' => 'no' 157 ), 158 'title' => array( 159 'title' => __( 'Title', 'woothemes' ), 160 'type' => 'text', 161 'description' => __( 'This controls the title which the user sees during checkout.', 'woothemes' ), 162 'default' => __( 'Credit Card (Inspire Commerce)', 'woothemes' ) 163 ), 164 'description' => array( 165 'title' => __( 'Description', 'woothemes' ), 166 'type' => 'textarea', 167 'description' => __( 'This controls the description which the user sees during checkout.', 'woothemes' ), 168 'default' => 'Pay with your credit card via Inspire Commerce.' 169 ), 170 'username' => array( 171 'title' => __( 'Username', 'woothemes' ), 172 'type' => 'text', 173 'description' => __( 'This is the API username generated within the Inspire Commerce gateway.', 'woothemes' ), 174 'default' => '' 175 ), 176 'password' => array( 177 'title' => __( 'Password', 'woothemes' ), 178 'type' => 'text', 179 'description' => __( 'This is the API user password generated within the Inspire Commerce gateway.', 'woothemes' ), 180 'default' => '' 181 ), 182 'salemethod' => array( 183 'title' => __( 'Sale Method', 'woothemes' ), 184 'type' => 'select', 185 'description' => __( 'Select which sale method to use. Authorize Only will authorize the customers card for the purchase amount only. Authorize & Capture will authorize the customer\'s card and collect funds.', 'woothemes' ), 186 'options' => array( 187 'sale' => 'Authorize & Capture', 188 'auth' => 'Authorize Only' 189 ), 190 'default' => 'Authorize & Capture' 191 ), 192 'cardtypes' => array( 193 'title' => __( 'Accepted Cards', 'woothemes' ), 194 'type' => 'multiselect', 195 'description' => __( 'Select which card types to accept.', 'woothemes' ), 196 'default' => '', 197 'options' => array( 198 'MasterCard' => 'MasterCard', 199 'Visa' => 'Visa', 200 'Discover' => 'Discover', 201 'American Express' => 'American Express' 202 ), 203 ), 204 'cvv' => array( 205 'title' => __( 'CVV', 'woothemes' ), 206 'type' => 'checkbox', 207 'label' => __( 'Require customer to enter credit card CVV code', 'woothemes' ), 208 'description' => __( '', 'woothemes' ), 209 'default' => 'yes' 210 ), 211 'saveinfo' => array( 212 'title' => __( 'Billing Information Storage', 'woothemes' ), 213 'type' => 'checkbox', 214 'label' => __( 'Allow customers to save billing information for future use (requires Inspire Commerce Customer Vault)', 'woothemes' ), 215 'description' => __( '', 'woothemes' ), 216 'default' => 'no' 217 ), 218 219 ); 220 } 221 222 223 /** 224 * UI - Admin Panel Options 225 */ 226 function admin_options() { ?> 227 <h3><?php _e( 'Inspire Commerce','woothemes' ); ?></h3> 228 <p><?php _e( 'The Inspire Commerce Gateway is simple and powerful. The plugin works by adding credit card fields on the checkout page, and then sending the details to Inspire Commerce for verification. <a href="http://www.inspirecommerce.com/woocommerce/">Click here to get paid like the pros</a>.', 'woothemes' ); ?></p> 229 <table class="form-table"> 230 <?php $this->generate_settings_html(); ?> 231 </table> 232 <?php } 233 /** 234 * UI - Payment page fields for Inspire Commerce. 235 */ 236 function payment_fields() { 237 // Description of payment method from settings 238 if ( $this->description ) { ?> 239 <p><?php echo $this->description; ?></p> 240 <?php } ?> 241 <fieldset style="padding-left: 40px;"> 242 <?php 243 $user = wp_get_current_user(); 244 $this->check_payment_method_conversion( $user->user_login, $user->ID ); 245 if ( $this->user_has_stored_data( $user->ID ) ) { ?> 246 <fieldset> 247 <input type="radio" name="inspire-use-stored-payment-info" id="inspire-use-stored-payment-info-yes" value="yes" checked="checked" onclick="document.getElementById('inspire-new-info').style.display='none'; document.getElementById('inspire-stored-info').style.display='block'"; /><label for="inspire-use-stored-payment-info-yes" style="display: inline;"><?php _e( 'Use a stored credit card', 'woocommerce' ) ?></label> 248 <div id="inspire-stored-info" style="padding: 10px 0 0 40px; clear: both;"> 249 <?php 250 $i = 0; 251 $method = $this->get_payment_method( $i ); 252 while( $method != null ) { 253 ?> 254 <p> 255 <input type="radio" name="inspire-payment-method" id="<?php echo $i; ?>" value="<?php echo $i; ?>" <?php if($i == 0){echo 'checked';}?>/> 256 <?php echo $method->cc_number; ?> (<?php 257 $exp = $method->cc_exp; 258 echo substr( $exp, 0, 2 ) . '/' . substr( $exp, -2 ); 259 ?>) 260 <br /> 261 </p> 262 <?php 263 $method = $this->get_payment_method( ++$i ); 264 } ?> 265 </fieldset> 266 <fieldset> 267 <p> 268 <input type="radio" name="inspire-use-stored-payment-info" id="inspire-use-stored-payment-info-no" value="no" onclick="document.getElementById('inspire-stored-info').style.display='none'; document.getElementById('inspire-new-info').style.display='block'"; /> 269 <label for="inspire-use-stored-payment-info-no" style="display: inline;"><?php _e( 'Use a new payment method', 'woocommerce' ) ?></label> 270 </p> 271 <div id="inspire-new-info" style="display:none"> 272 </fieldset> 273 <?php } else { ?> 274 <fieldset> 275 <!-- Show input boxes for new data --> 276 <div id="inspire-new-info"> 277 <?php } ?> 278 <!-- Credit card number --> 279 <p class="form-row form-row-first"> 280 <label for="ccnum"><?php echo __( 'Credit Card number', 'woocommerce' ) ?> <span class="required">*</span></label> 281 <input type="text" class="input-text" id="ccnum" name="ccnum" maxlength="16" /> 282 </p> 283 <!-- Credit card type --> 284 <p class="form-row form-row-last"> 285 <label for="cardtype"><?php echo __( 'Card type', 'woocommerce' ) ?> <span class="required">*</span></label> 286 <select name="cardtype" id="cardtype" class="woocommerce-select"> 287 <?php foreach( $this->cardtypes as $type ) { ?> 288 <option value="<?php echo $type ?>"><?php _e( $type, 'woocommerce' ); ?></option> 289 <?php } ?> 290 </select> 291 </p> 292 <div class="clear"></div> 293 <!-- Credit card expiration --> 294 <p class="form-row form-row-first"> 295 <label for="cc-expire-month"><?php echo __( 'Expiration date', 'woocommerce') ?> <span class="required">*</span></label> 296 <select name="expmonth" id="expmonth" class="woocommerce-select woocommerce-cc-month"> 297 <option value=""><?php _e( 'Month', 'woocommerce' ) ?></option><?php 298 $months = array(); 299 for ( $i = 1; $i <= 12; $i ++ ) { 300 $timestamp = mktime( 0, 0, 0, $i, 1 ); 301 $months[ date( 'n', $timestamp ) ] = date( 'F', $timestamp ); 302 } 303 foreach ( $months as $num => $name ) { 304 printf( '<option value="%u">%s</option>', $num, $name ); 305 } ?> 306 </select> 307 <select name="expyear" id="expyear" class="woocommerce-select woocommerce-cc-year"> 308 <option value=""><?php _e( 'Year', 'woocommerce' ) ?></option><?php 309 $years = array(); 310 for ( $i = date( 'y' ); $i <= date( 'y' ) + 15; $i ++ ) { 311 printf( '<option value="20%u">20%u</option>', $i, $i ); 312 } ?> 313 </select> 314 </p> 315 <?php 316 317 // Credit card security code 318 if ( $this->cvv == 'yes' ) { ?> 319 <p class="form-row form-row-last"> 320 <label for="cvv"><?php _e( 'Card security code', 'woocommerce' ) ?> <span class="required">*</span></label> 321 <input oninput="validate_cvv(this.value)" type="text" class="input-text" id="cvv" name="cvv" maxlength="4" style="width:45px" /> 322 <span class="help"><?php _e( '3 or 4 digits usually found on the signature strip.', 'woocommerce' ) ?></span> 323 </p><?php 324 } 325 326 // Option to store credit card data 327 if ( $this->saveinfo == 'yes' && ! ( class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) ) { ?> 328 <div style="clear: both;"></div> 329 <p> 330 <label for="saveinfo"><?php _e( 'Save this billing method?', 'woocommerce' ) ?></label> 331 <input type="checkbox" class="input-checkbox" id="saveinfo" name="saveinfo" /> 332 <span class="help"><?php _e( 'Select to store your billing information for future use.', 'woocommerce' ) ?></span> 333 </p> 334 <?php } ?> 335 </fieldset> 336 </fieldset> 337 <?php 338 } 339 340 /** 341 * Process the payment and return the result. 342 */ 343 function process_payment( $order_id ) { 344 345 global $woocommerce; 346 347 $order = new WC_Order( $order_id ); 348 $user = new WP_User( $order->user_id ); 349 $this->check_payment_method_conversion( $user->user_login, $user->ID ); 350 351 // Convert CC expiration date from (M)M-YYYY to MMYY 352 $expmonth = $this->get_post( 'expmonth' ); 353 if ( $expmonth < 10 ) $expmonth = '0' . $expmonth; 354 if ( $this->get_post( 'expyear' ) != null ) $expyear = substr( $this->get_post( 'expyear' ), -2 ); 355 356 // Create server request using stored or new payment details 357 if ( $this->get_post( 'inspire-use-stored-payment-info' ) == 'yes' ) { 358 359 // Short request, use stored billing details 360 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 361 $id = $customer_vault_ids[ $this->get_post( 'inspire-payment-method' ) ]; 362 if( substr( $id, 0, 1 ) !== '_' ) $base_request['customer_vault_id'] = $id; 363 else { 364 $base_request['customer_vault_id'] = $user->user_login; 365 $base_request['billing_id'] = substr( $id , 1 ); 366 $base_request['ver'] = 2; 367 } 368 369 } else { 370 371 // Full request, new customer or new information 372 $base_request = array ( 373 'ccnumber' => $this->get_post( 'ccnum' ), 374 'cvv' => $this->get_post( 'cvv' ), 375 'ccexp' => $expmonth . $expyear, 376 'firstname' => $order->billing_first_name, 377 'lastname' => $order->billing_last_name, 378 'address1' => $order->billing_address_1, 379 'city' => $order->billing_city, 380 'state' => $order->billing_state, 381 'zip' => $order->billing_postcode, 382 'country' => $order->billing_country, 383 'phone' => $order->billing_phone, 384 'email' => $order->billing_email, 385 ); 386 387 // If "save billing data" box is checked or order is a subscription, also request storage of customer payment information. 388 if ( $this->get_post( 'saveinfo' ) || $this->is_subscription( $order ) ) { 389 390 $base_request['customer_vault'] = 'add_customer'; 391 392 // Generate a new customer vault id for the payment method 393 $new_customer_vault_id = $this->random_key(); 394 395 // Set customer ID for new record 396 $base_request['customer_vault_id'] = $new_customer_vault_id; 397 398 // Set 'recurring' flag for subscriptions 399 if( $this->is_subscription( $order ) ) $base_request['billing_method'] = 'recurring'; 400 401 } 402 } 403 404 // Add transaction-specific details to the request 405 $transaction_details = array ( 406 'username' => $this->username, 407 'password' => $this->password, 408 'amount' => $order->order_total, 409 'type' => $this->salemethod, 410 'payment' => 'creditcard', 411 'orderid' => $order->id, 412 'ipaddress' => $_SERVER['REMOTE_ADDR'], 413 ); 414 415 // Send request and get response from server 416 $response = $this->post_and_get_response( array_merge( $base_request, $transaction_details ) ); 417 418 // Check response 419 if ( $response['response'] == 1 ) { 420 // Success 421 $order->add_order_note( __( 'Inspire Commerce payment completed. Transaction ID: ' , 'woocommerce' ) . $response['transactionid'] ); 422 $order->payment_complete(); 423 424 if ( $this->get_post( 'inspire-use-stored-payment-info' ) == 'yes' ) { 425 426 if ( $this->is_subscription( $order ) ) { 427 // Store payment method number for future subscription payments 428 update_post_meta( $order->id, 'payment_method_number', $this->get_post( 'inspire-payment-method' ) ); 429 } 430 431 } else if ( $this->get_post( 'saveinfo' ) || $this->is_subscription( $order ) ) { 432 433 // Store the payment method number/customer vault ID translation table in the user's metadata 434 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 435 $customer_vault_ids[] = $new_customer_vault_id; 436 update_user_meta( $user->ID, 'customer_vault_ids', $customer_vault_ids ); 437 438 if ( $this->is_subscription( $order ) ) { 439 // Store payment method number for future subscription payments 440 update_post_meta( $order->id, 'payment_method_number', count( $customer_vault_ids ) - 1 ); 441 } 442 443 } 444 445 // Return thank you redirect 446 return array ( 447 'result' => 'success', 448 'redirect' => $this->get_return_url( $order ), 449 ); 450 451 } else if ( $response['response'] == 2 ) { 452 // Decline 453 $order->add_order_note( __( 'Inspire Commerce payment failed. Payment declined.', 'woocommerce' ) ); 454 wc_add_notice( __( 'Sorry, the transaction was declined.', 'woocommerce' ), $notice_type = 'error' ); 455 456 } else if ( $response['response'] == 3 ) { 457 // Other transaction error 458 $order->add_order_note( __( 'Inspire Commerce payment failed. Error: ', 'woocommerce' ) . $response['responsetext'] ); 459 wc_add_notice( __( 'Sorry, there was an error: ', 'woocommerce' ) . $response['responsetext'], $notice_type = 'error' ); 460 461 } else { 462 // No response or unexpected response 463 $order->add_order_note( __( "Inspire Commerce payment failed. Couldn't connect to gateway server.", 'woocommerce' ) ); 464 wc_add_notice( __( 'No response from payment gateway server. Try again later or contact the site administrator.', 'woocommerce' ), $notice_type = 'error' ); 465 466 } 467 468 } 469 470 /** 471 * Process a payment for an ongoing subscription. 472 */ 473 function process_scheduled_subscription_payment( $amount_to_charge, $order, $product_id ) { 474 475 $user = new WP_User( $order->user_id ); 476 $this->check_payment_method_conversion( $user->user_login, $user->ID ); 477 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 478 $payment_method_number = get_post_meta( $order->id, 'payment_method_number', true ); 479 480 $inspire_request = array ( 481 'username' => $this->username, 482 'password' => $this->password, 483 'amount' => $amount_to_charge, 484 'type' => $this->salemethod, 485 'billing_method' => 'recurring', 486 ); 487 488 $id = $customer_vault_ids[ $payment_method_number ]; 489 if( substr( $id, 0, 1 ) !== '_' ) $inspire_request['customer_vault_id'] = $id; 490 else { 491 $inspire_request['customer_vault_id'] = $user->user_login; 492 $inspire_request['billing_id'] = substr( $id , 1 ); 493 $inspire_request['ver'] = 2; 494 } 495 496 $response = $this->post_and_get_response( $inspire_request ); 497 498 if ( $response['response'] == 1 ) { 499 // Success 500 $order->add_order_note( __( 'Inspire Commerce scheduled subscription payment completed. Transaction ID: ' , 'woocommerce' ) . $response['transactionid'] ); 501 WC_Subscriptions_Manager::process_subscription_payments_on_order( $order ); 502 503 } else if ( $response['response'] == 2 ) { 504 // Decline 505 $order->add_order_note( __( 'Inspire Commerce scheduled subscription payment failed. Payment declined.', 'woocommerce') ); 506 WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $order ); 507 508 } else if ( $response['response'] == 3 ) { 509 // Other transaction error 510 $order->add_order_note( __( 'Inspire Commerce scheduled subscription payment failed. Error: ', 'woocommerce') . $response['responsetext'] ); 511 WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $order ); 512 513 } else { 514 // No response or unexpected response 515 $order->add_order_note( __('Inspire Commerce scheduled subscription payment failed. Couldn\'t connect to gateway server.', 'woocommerce') ); 516 517 } 518 } 519 520 /** 521 * Get details of a payment method for the current user from the Customer Vault 522 */ 523 function get_payment_method( $payment_method_number ) { 524 525 if( $payment_method_number < 0 ) die( 'Invalid payment method: ' . $payment_method_number ); 526 527 $user = wp_get_current_user(); 528 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 529 if( $payment_method_number >= count( $customer_vault_ids ) ) return null; 530 531 $query = array ( 532 'username' => $this->username, 533 'password' => $this->password, 534 'report_type' => 'customer_vault', 535 ); 536 537 $id = $customer_vault_ids[ $payment_method_number ]; 538 if( substr( $id, 0, 1 ) !== '_' ) $query['customer_vault_id'] = $id; 539 else { 540 $query['customer_vault_id'] = $user->user_login; 541 $query['billing_id'] = substr( $id , 1 ); 542 $query['ver'] = 2; 543 } 544 $response = wp_remote_post( QUERY_URL, array( 545 'body' => $query, 546 'timeout' => 45, 547 'redirection' => 5, 548 'httpversion' => '1.0', 549 'blocking' => true, 550 'headers' => array(), 551 'cookies' => array(), 552 'ssl_verify' => false 553 ) 554 ); 555 556 //Do we have an error? 557 if( is_wp_error( $response ) ) return null; 558 559 // Check for empty response, which means method does not exist 560 if ( trim( strip_tags( $response['body'] ) ) == '' ) return null; 561 562 // Format result 563 $content = simplexml_load_string( $response['body'] )->customer_vault->customer; 564 if( substr( $id, 0, 1 ) === '_' ) $content = $content->billing; 565 566 return $content; 567 } 568 569 /** 570 * Check if a user's stored billing records have been converted to Single Billing. If not, do it now. 571 */ 572 function check_payment_method_conversion( $user_login, $user_id ) { 573 if( ! $this->user_has_stored_data( $user_id ) && $this->get_mb_payment_methods( $user_login ) != null ) $this->convert_mb_payment_methods( $user_login, $user_id ); 574 } 575 576 /** 577 * Convert any Multiple Billing records stored by the user into Single Billing records 578 */ 579 function convert_mb_payment_methods( $user_login, $user_id ) { 580 581 $mb_methods = $this->get_mb_payment_methods( $user_login ); 582 foreach ( $mb_methods->billing as $method ) $customer_vault_ids[] = '_' . ( (string) $method['id'] ); 583 // Store the payment method number/customer vault ID translation table in the user's metadata 584 add_user_meta( $user_id, 'customer_vault_ids', $customer_vault_ids ); 585 586 // Update subscriptions to reference the new records 587 if( class_exists( 'WC_Subscriptions_Manager' ) ) { 588 589 $payment_method_numbers = array_flip( $customer_vault_ids ); 590 foreach( (array) ( WC_Subscriptions_Manager::get_users_subscriptions( $user_id ) ) as $subscription ) { 591 update_post_meta( $subscription['order_id'], 'payment_method_number', $payment_method_numbers[ '_' . get_post_meta( $subscription['order_id'], 'billing_id', true ) ] ); 592 delete_post_meta( $subscription['order_id'], 'billing_id' ); 593 } 594 595 } 596 } 597 598 /** 599 * Get the user's Multiple Billing records from the Customer Vault 600 */ 601 function get_mb_payment_methods( $user_login ) { 602 603 if( $user_login == null ) return null; 604 605 $query = array ( 606 'username' => $this->username, 607 'password' => $this->password, 608 'report_type' => 'customer_vault', 609 'customer_vault_id' => $user_login, 610 'ver' => '2', 611 ); 612 $content = wp_remote_post( QUERY_URL, array( 613 'body' => $query, 614 'timeout' => 45, 615 'redirection' => 5, 616 'httpversion' => '1.0', 617 'blocking' => true, 618 'headers' => array(), 619 'cookies' => array(), 620 'ssl_verify' => false 621 ) 622 ); 623 624 if ( trim( strip_tags( $content['body'] ) ) == '' ) return null; 625 return simplexml_load_string( $content['body'] )->customer_vault->customer; 626 627 } 628 629 /** 630 * Check if the user has any billing records in the Customer Vault 631 */ 632 function user_has_stored_data( $user_id ) { 633 return get_user_meta( $user_id, 'customer_vault_ids', true ) != null; 634 } 635 636 /** 637 * Update a stored billing record with new CC number and expiration 638 */ 639 function update_payment_method( $payment_method, $ccnumber, $ccexp ) { 640 641 global $woocommerce; 642 $user = wp_get_current_user(); 643 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 644 645 $id = $customer_vault_ids[ $payment_method ]; 646 if( substr( $id, 0, 1 ) == '_' ) { 647 // Copy all fields from the Multiple Billing record 648 $mb_method = $this->get_payment_method( $payment_method ); 649 $inspire_request = (array) $mb_method[0]; 650 // Make sure values are strings 651 foreach( $inspire_request as $key => $val ) $inspire_request[ $key ] = "$val"; 652 // Add a new record with the updated details 653 $inspire_request['customer_vault'] = 'add_customer'; 654 $new_customer_vault_id = $this->random_key(); 655 $inspire_request['customer_vault_id'] = $new_customer_vault_id; 656 } else { 657 // Update existing record 658 $inspire_request['customer_vault'] = 'update_customer'; 659 $inspire_request['customer_vault_id'] = $id; 660 } 661 662 $inspire_request['username'] = $this->username; 663 $inspire_request['password'] = $this->password; 664 // Overwrite updated fields 665 $inspire_request['cc_number'] = $ccnumber; 666 $inspire_request['cc_exp'] = $ccexp; 667 668 $response = $this->post_and_get_response( $inspire_request ); 669 670 if( $response ['response'] == 1 ) { 671 if( substr( $id, 0, 1 ) === '_' ) { 672 // Update references 673 $customer_vault_ids[ $payment_method ] = $new_customer_vault_id; 674 update_user_meta( $user->ID, 'customer_vault_ids', $customer_vault_ids ); 675 } 676 wc_add_notice( __('Successfully updated your information!', 'woocommerce'), $notice_type = 'success' ); 677 } else wc_add_notice( __( 'Sorry, there was an error: ', 'woocommerce') . $response['responsetext'], $notice_type = 'error' ); 678 679 } 680 681 /** 682 * Delete a stored billing method 683 */ 684 function delete_payment_method( $payment_method ) { 685 686 global $woocommerce; 687 $user = wp_get_current_user(); 688 $customer_vault_ids = get_user_meta( $user->ID, 'customer_vault_ids', true ); 689 690 $id = $customer_vault_ids[ $payment_method ]; 691 // If method is Single Billing, actually delete the record 692 if( substr( $id, 0, 1 ) !== '_' ) { 693 694 $inspire_request = array ( 695 'username' => $this->username, 696 'password' => $this->password, 697 'customer_vault' => 'delete_customer', 698 'customer_vault_id' => $id, 699 ); 700 $response = $this->post_and_get_response( $inspire_request ); 701 if( $response['response'] != 1 ) { 702 wc_add_notice( __( 'Sorry, there was an error: ', 'woocommerce') . $response['responsetext'], $notice_type = 'error' ); 703 return; 704 } 705 706 } 707 708 $last_method = count( $customer_vault_ids ) - 1; 709 710 // Update subscription references 711 if( class_exists( 'WC_Subscriptions_Manager' ) ) { 712 foreach( (array) ( WC_Subscriptions_Manager::get_users_subscriptions( $user->ID ) ) as $subscription ) { 713 $subscription_payment_method = get_post_meta( $subscription['order_id'], 'payment_method_number', true ); 714 // Cancel subscriptions that were purchased with the deleted method 715 if( $subscription_payment_method == $payment_method ) { 716 delete_post_meta( $subscription['order_id'], 'payment_method_number' ); 717 WC_Subscriptions_Manager::cancel_subscription( $user->ID, WC_Subscriptions_Manager::get_subscription_key( $subscription['order_id'] ) ); 718 } 719 else if( $subscription_payment_method == $last_method && $subscription['status'] != 'cancelled') { 720 update_post_meta( $subscription['order_id'], 'payment_method_number', $payment_method ); 721 } 722 } 723 } 724 725 // Delete the reference by replacing it with the last method in the array 726 if( $payment_method < $last_method ) $customer_vault_ids[ $payment_method ] = $customer_vault_ids[ $last_method ]; 727 unset( $customer_vault_ids[ $last_method ] ); 728 update_user_meta( $user->ID, 'customer_vault_ids', $customer_vault_ids ); 729 730 wc_add_notice( __('Successfully deleted your information!', 'woocommerce'), $notice_type = 'success' ); 731 732 } 733 734 /** 735 * Check payment details for valid format 736 */ 737 function validate_fields() { 738 739 if ( $this->get_post( 'inspire-use-stored-payment-info' ) == 'yes' ) return true; 740 741 global $woocommerce; 742 743 // Check for saving payment info without having or creating an account 744 if ( $this->get_post( 'saveinfo' ) && ! is_user_logged_in() && ! $this->get_post( 'createaccount' ) ) { 745 wc_add_notice( __( 'Sorry, you need to create an account in order for us to save your payment information.', 'woocommerce'), $notice_type = 'error' ); 746 return false; 747 } 748 749 $cardType = $this->get_post( 'cardtype' ); 750 $cardNumber = $this->get_post( 'ccnum' ); 751 $cardCSC = $this->get_post( 'cvv' ); 752 $cardExpirationMonth = $this->get_post( 'expmonth' ); 753 $cardExpirationYear = $this->get_post( 'expyear' ); 754 755 // Check card number 756 if ( empty( $cardNumber ) || ! ctype_digit( $cardNumber ) ) { 757 wc_add_notice( __( 'Card number is invalid.', 'woocommerce' ), $notice_type = 'error' ); 758 return false; 759 } 760 761 if ( $this->cvv == 'yes' ){ 762 // Check security code 763 if ( ! ctype_digit( $cardCSC ) ) { 764 wc_add_notice( __( 'Card security code is invalid (only digits are allowed).', 'woocommerce' ), $notice_type = 'error' ); 765 return false; 766 } 767 if ( ( strlen( $cardCSC ) != 3 && in_array( $cardType, array( 'Visa', 'MasterCard', 'Discover' ) ) ) || ( strlen( $cardCSC ) != 4 && $cardType == 'American Express' ) ) { 768 wc_add_notice( __( 'Card security code is invalid (wrong length).', 'woocommerce' ), $notice_type = 'error' ); 769 return false; 770 } 771 } 772 773 // Check expiration data 774 $currentYear = date( 'Y' ); 775 776 if ( ! ctype_digit( $cardExpirationMonth ) || ! ctype_digit( $cardExpirationYear ) || 777 $cardExpirationMonth > 12 || 778 $cardExpirationMonth < 1 || 779 $cardExpirationYear < $currentYear || 780 $cardExpirationYear > $currentYear + 20 781 ) { 782 wc_add_notice( __( 'Card expiration date is invalid', 'woocommerce' ), $notice_type = 'error' ); 783 return false; 784 } 785 786 // Strip spaces and dashes 787 $cardNumber = str_replace( array( ' ', '-' ), '', $cardNumber ); 788 789 return true; 790 791 } 792 793 /** 794 * Send the payment data to the gateway server and return the response. 795 */ 796 private function post_and_get_response( $request ) { 797 global $woocommerce; 798 799 // Encode request 800 $post = http_build_query( $request, '', '&' ); 801 802 // Send request 803 $content = wp_remote_post( GATEWAY_URL, array( 804 'body' => $post, 805 'timeout' => 45, 806 'redirection' => 5, 807 'httpversion' => '1.0', 808 'blocking' => true, 809 'headers' => array(), 810 'cookies' => array(), 811 'ssl_verify' => false 812 ) 813 ); 814 815 // Quit if it didn't work 816 if ( is_wp_error( $content ) ) { 817 wc_add_notice( __( 'Problem connecting to server at ', 'woocommerce' ) . GATEWAY_URL . ' ( ' . $content->get_error_message() . ' )', $notice_type = 'error' ); 818 return null; 819 } 820 821 // Convert response string to array 822 $vars = explode( '&', $content['body'] ); 823 foreach ( $vars as $key => $val ) { 824 $var = explode( '=', $val ); 825 $data[ $var[0] ] = $var[1]; 826 } 827 828 // Return response array 829 return $data; 830 831 } 832 833 /** 834 * Add ability to view and edit payment details on the My Account page.(The WooCommerce 'force ssl' option also secures the My Account page, so we don't need to do that.) 835 */ 836 function add_payment_method_options() { 837 838 $user = wp_get_current_user(); 839 $this->check_payment_method_conversion( $user->user_login, $user->ID ); 840 if ( ! $this->user_has_stored_data( $user->ID ) ) return; 841 842 if( $this->get_post( 'delete' ) != null ) { 843 844 $method_to_delete = $this->get_post( 'delete' ); 845 $response = $this->delete_payment_method( $method_to_delete ); 846 847 } else if( $this->get_post( 'update' ) != null ) { 848 849 $method_to_update = $this->get_post( 'update' ); 850 $ccnumber = $this->get_post( 'edit-cc-number-' . $method_to_update ); 851 852 if ( empty( $ccnumber ) || ! ctype_digit( $ccnumber ) ) { 853 854 global $woocommerce; 855 wc_add_notice( __( 'Card number is invalid.', 'woocommerce' ), $notice_type = 'error' ); 856 857 } else { 858 859 $ccexp = $this->get_post( 'edit-cc-exp-' . $method_to_update ); 860 $expmonth = substr( $ccexp, 0, 2 ); 861 $expyear = substr( $ccexp, -2 ); 862 $currentYear = substr( date( 'Y' ), -2); 863 864 if( empty( $ccexp ) || ! ctype_digit( str_replace( '/', '', $ccexp ) ) || 865 $expmonth > 12 || $expmonth < 1 || 866 $expyear < $currentYear || $expyear > $currentYear + 20 ) 867 { 868 869 global $woocommerce; 870 wc_add_notice( __( 'Card expiration date is invalid', 'woocommerce' ), $notice_type = 'error' ); 871 872 } else { 873 874 $response = $this->update_payment_method( $method_to_update, $ccnumber, $ccexp ); 875 876 } 877 } 878 } 879 880 ?> 881 882 <h2>Saved Payment Methods</h2> 883 <p>This information is stored to save time at the checkout and to pay for subscriptions.</p> 884 885 <?php $i = 0; 886 $current_method = $this->get_payment_method( $i ); 887 while( $current_method != null ) { 888 889 if( $method_to_delete === $i && $response['response'] == 1 ) { $method_to_delete = null; continue; } // Skip over a deleted entry ?> 890 891 <header class="title"> 892 893 <h3> 894 Payment Method <?php echo $i + 1; ?> 895 </h3> 896 <p> 897 898 <button style="float:right" class="button" id="unlock-delete-button-<?php echo $i; ?>"><?php _e( 'Delete', 'woocommerce' ); ?></button> 899 900 <button style="float:right; display:none" class="button" id="cancel-delete-button-<?php echo $i; ?>"><?php _e( 'No', 'woocommerce' ); ?></button> 901 <form action="<?php echo get_permalink( woocommerce_get_page_id( 'myaccount' ) ) ?>" method="post" style="float:right" > 902 <input type="submit" value="<?php _e( 'Yes', 'woocommerce' ); ?>" class="button alt" id="delete-button-<?php echo $i; ?>" style="display:none"> 903 <input type="hidden" name="delete" value="<?php echo $i ?>"> 904 </form> 905 <span id="delete-confirm-msg-<?php echo $i; ?>" style="float:left_; display:none">Are you sure? (Subscriptions purchased with this card will be canceled.) </span> 906 907 <button style="float:right" class="button" id="edit-button-<?php echo $i; ?>" ><?php _e( 'Edit', 'woocommerce' ); ?></button> 908 <button style="float:right; display:none" class="button" id="cancel-button-<?php echo $i; ?>" ><?php _e( 'Cancel', 'woocommerce' ); ?></button> 909 910 <form action="<?php echo get_permalink( woocommerce_get_page_id( 'myaccount' ) ) ?>" method="post" > 911 912 <input type="submit" value="<?php _e( 'Save', 'woocommerce' ); ?>" class="button alt" id="save-button-<?php echo $i; ?>" style="float:right; display:none" > 913 914 <span style="float:left">Credit card: </span> 915 <input type="text" style="display:none" id="edit-cc-number-<?php echo $i; ?>" name="edit-cc-number-<?php echo $i; ?>" maxlength="16" /> 916 <span id="cc-number-<?php echo $i; ?>"> 917 <?php echo ( $method_to_update === $i && $response['response'] == 1 ) ? ( '<b>' . $ccnumber . '</b>' ) : $current_method->cc_number; ?> 918 </span> 919 <br /> 920 921 <span style="float:left">Expiration: </span> 922 <input type="text" style="float:left; display:none" id="edit-cc-exp-<?php echo $i; ?>" name="edit-cc-exp-<?php echo $i; ?>" maxlength="5" value="MM/YY" /> 923 <span id="cc-exp-<?php echo $i; ?>"> 924 <?php echo ( $method_to_update === $i && $response['response'] == 1 ) ? ( '<b>' . $ccexp . '</b>' ) : substr( $current_method->cc_exp, 0, 2 ) . '/' . substr( $current_method->cc_exp, -2 ); ?> 925 </span> 926 927 <input type="hidden" name="update" value="<?php echo $i ?>"> 928 929 </form> 930 931 </p> 932 933 </header><?php 934 935 $current_method = $this->get_payment_method( ++$i ); 936 937 } 938 939 } 940 941 function receipt_page( $order ) { 942 echo '<p>' . __( 'Thank you for your order.', 'woocommerce' ) . '</p>'; 943 } 944 945 /** 946 * Include jQuery and our scripts 947 */ 948 function add_inspire_scripts() { 949 950 if ( ! $this->user_has_stored_data( wp_get_current_user()->ID ) ) return; 951 952 wp_enqueue_script( 'jquery' ); 953 wp_enqueue_script( 'edit_billing_details', PLUGIN_DIR . 'js/edit_billing_details.js', array( 'jquery' ), 1.0 ); 954 955 if ( $this->cvv == 'yes' ) wp_enqueue_script( 'check_cvv', PLUGIN_DIR . 'js/check_cvv.js', array( 'jquery' ), 1.0 ); 956 957 } 958 959 /** 960 * Get the current user's login name 961 */ 962 private function get_user_login() { 963 global $user_login; 964 get_currentuserinfo(); 965 return $user_login; 966 } 967 968 /** 969 * Get post data if set 970 */ 971 private function get_post( $name ) { 972 if ( isset( $_POST[ $name ] ) ) { 973 return $_POST[ $name ]; 974 } 975 return null; 976 } 977 978 /** 979 * Check whether an order is a subscription 980 */ 981 private function is_subscription( $order ) { 982 return class_exists( 'WC_Subscriptions_Order' ) && WC_Subscriptions_Order::order_contains_subscription( $order ); 983 } 984 985 /** 986 * Generate a string of 36 alphanumeric characters to associate with each saved billing method. 987 */ 988 function random_key() { 989 990 $valid_chars = array( 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9' ); 991 $key = ''; 992 for( $i = 0; $i < 36; $i ++ ) { 993 $key .= $valid_chars[ mt_rand( 0, 61 ) ]; 994 } 995 return $key; 996 997 } 37 // Actions 38 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'plugin_action_links' ) ); 39 add_action( 'plugins_loaded', array( $this, 'init' ), 0 ); 40 add_filter( 'woocommerce_payment_gateways', array( $this, 'register_gateway' ) ); 41 add_action( 'wp_enqueue_scripts', array( $this, 'add_inspire_scripts' ) ); 998 42 999 43 } 1000 44 1001 45 /** 1002 * Add the gateway to woocommerce 46 * Add links to plugins page for settings and documentation 47 * @param array $links 48 * @return array 1003 49 */ 1004 function add_inspire_commerce_gateway( $methods ) { 1005 $methods[] = 'WC_Inspire'; 1006 return $methods; 50 public function plugin_action_links( $links ) { 51 $subscriptions = ( class_exists( 'WC_Subscriptions_Order' ) ) ? '_subscriptions' : ''; 52 if ( class_exists( 'WC_Subscriptions_Order' ) && ! function_exists( 'wcs_create_renewal_order' ) ) { 53 $subscriptions = '_subscriptions_deprecated'; 54 } 55 $plugin_links = array( 56 '<a href="' . admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=wc_gateway_inspire' . $subscriptions ) . '">' . __( 'Settings', 'woocommerce-gateway-inspire' ) . '</a>', 57 '<a href="http://www.inspirecommerce.com/woocommerce/">' . __( 'Support', 'woocommerce-gateway-inspire' ) . '</a>', 58 '<a href="http://www.inspirecommerce.com/woocommerce/">' . __( 'Docs', 'woocommerce-gateway-inspire' ) . '</a>', 59 ); 60 return array_merge( $plugin_links, $links ); 1007 61 } 1008 62 1009 add_filter( 'woocommerce_payment_gateways', 'add_inspire_commerce_gateway' ); 63 /** 64 * Init localisations and files 65 */ 66 public function init() { 67 68 if ( ! class_exists( 'WC_Payment_Gateway' ) ) { 69 return; 70 } 71 72 // Includes 73 include_once( 'includes/class-wc-gateway-inspire.php' ); 74 75 if ( class_exists( 'WC_Subscriptions_Order' ) ) { 76 77 include_once( 'includes/class-wc-gateway-inspire-subscriptions.php' ); 78 79 } 80 81 // Localisation 82 load_plugin_textdomain( 'woocommerce-gateway-inspire', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); 83 84 } 85 86 /** 87 * Register the gateway for use 88 */ 89 public function register_gateway( $methods ) { 90 91 if ( class_exists( 'WC_Subscriptions_Order' ) ) { 92 93 $methods[] = 'WC_Gateway_Inspire_Subscriptions'; 94 95 } else { 96 $methods[] = 'WC_Gateway_Inspire'; 97 } 98 99 return $methods; 100 101 } 102 103 104 /** 105 * Include jQuery and our scripts 106 */ 107 function add_inspire_scripts() { 108 109 if ( ! $this->user_has_stored_data( wp_get_current_user()->ID ) ) return; 110 wp_enqueue_script( 'edit_billing_details', WC_INSPIRE_PLUGIN_DIR . 'js/edit_billing_details.js', array( 'jquery' ), WC_INSPIRE_VERSION ); 111 wp_enqueue_script( 'check_cvv', WC_INSPIRE_PLUGIN_DIR . 'js/check_cvv.js', array( 'jquery' ), WC_INSPIRE_VERSION ); 112 113 } 114 115 /** 116 * Check if the user has any billing records in the Customer Vault 117 * 118 * @param $user_id 119 * 120 * @return bool 121 */ 122 function user_has_stored_data( $user_id ) { 123 return get_user_meta( $user_id, 'customer_vault_ids', true ) != null; 124 } 125 1010 126 1011 127 } 128 129 new WC_Inspire(); -
woocommerce-payment-gateway/trunk/readme.txt
r1100709 r1573562 4 4 Tags: WooCommerce, Payment, Gateway, Credit Cards, Shopping Cart, Inspire, Inspire Commerce, Extension, Subscriptions, Recurring Billing, Membership 5 5 Requires at least: 3.0.0 6 Tested up to: 4. 1.17 Stable tag: 1.7.66 Tested up to: 4.7.1 7 Stable tag: 2.0.0 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 91 91 92 92 == Changelog == 93 94 = 2.0.0 = 95 * Compatibility with WooCommerce 2.6.x and Subscriptions 2.x 93 96 94 97 = 1.7.6 =
Note: See TracChangeset
for help on using the changeset viewer.