PageRenderTime 54ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/woocommerce/includes/gateways/paypal/class-wc-gateway-paypal.php

https://bitbucket.org/theshipswakecreative/psw
PHP | 1064 lines | 754 code | 151 blank | 159 comment | 142 complexity | 6a048b2e3e69cb8242da3bd2ac4d3058 MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0
  1. <?php
  2. if ( ! defined( 'ABSPATH' ) ) {
  3. exit; // Exit if accessed directly
  4. }
  5. /**
  6. * PayPal Standard Payment Gateway
  7. *
  8. * Provides a PayPal Standard Payment Gateway.
  9. *
  10. * @class WC_Paypal
  11. * @extends WC_Gateway_Paypal
  12. * @version 2.0.0
  13. * @package WooCommerce/Classes/Payment
  14. * @author WooThemes
  15. */
  16. class WC_Gateway_Paypal extends WC_Payment_Gateway {
  17. var $notify_url;
  18. /**
  19. * Constructor for the gateway.
  20. */
  21. public function __construct() {
  22. $this->id = 'paypal';
  23. $this->has_fields = false;
  24. $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' );
  25. $this->liveurl = 'https://www.paypal.com/cgi-bin/webscr';
  26. $this->testurl = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  27. $this->method_title = __( 'PayPal', 'woocommerce' );
  28. $this->method_description = __( 'PayPal standard works by sending the user to PayPal to enter their payment information.', 'woocommerce' );
  29. $this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' );
  30. $this->supports = array(
  31. 'products',
  32. 'refunds'
  33. );
  34. // Load the settings.
  35. $this->init_form_fields();
  36. $this->init_settings();
  37. // Define user set variables
  38. $this->title = $this->get_option( 'title' );
  39. $this->description = $this->get_option( 'description' );
  40. $this->email = $this->get_option( 'email' );
  41. $this->receiver_email = $this->get_option( 'receiver_email', $this->email );
  42. $this->testmode = $this->get_option( 'testmode' );
  43. $this->send_shipping = $this->get_option( 'send_shipping' );
  44. $this->address_override = $this->get_option( 'address_override' );
  45. $this->debug = $this->get_option( 'debug' );
  46. $this->page_style = $this->get_option( 'page_style' );
  47. $this->invoice_prefix = $this->get_option( 'invoice_prefix', 'WC-' );
  48. $this->paymentaction = $this->get_option( 'paymentaction', 'sale' );
  49. $this->identity_token = $this->get_option( 'identity_token', '' );
  50. $this->api_username = $this->get_option( 'api_username' );
  51. $this->api_password = $this->get_option( 'api_password' );
  52. $this->api_signature = $this->get_option( 'api_signature' );
  53. // Logs
  54. if ( 'yes' == $this->debug ) {
  55. $this->log = new WC_Logger();
  56. }
  57. // Actions
  58. add_action( 'valid-paypal-standard-ipn-request', array( $this, 'successful_request' ) );
  59. add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
  60. add_action( 'woocommerce_thankyou_paypal', array( $this, 'pdt_return_handler' ) );
  61. // Payment listener/API hook
  62. add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_ipn_response' ) );
  63. if ( ! $this->is_valid_for_use() ) {
  64. $this->enabled = 'no';
  65. }
  66. }
  67. /**
  68. * get_icon function.
  69. *
  70. * @return string
  71. */
  72. public function get_icon() {
  73. $link = null;
  74. switch ( WC()->countries->get_base_country() ) {
  75. case 'US' :
  76. case 'NZ' :
  77. case 'CZ' :
  78. case 'HU' :
  79. case 'MY' :
  80. $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg';
  81. break;
  82. case 'TR' :
  83. $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg';
  84. break;
  85. case 'GB' :
  86. $icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png';
  87. break;
  88. case 'MX' :
  89. $icon = array(
  90. 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png',
  91. 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif'
  92. );
  93. $link = 'https://www.paypal.com/mx/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside';
  94. break;
  95. case 'FR' :
  96. $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg';
  97. break;
  98. case 'AU' :
  99. $icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg';
  100. break;
  101. case 'DK' :
  102. $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg';
  103. break;
  104. case 'RU' :
  105. $icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg';
  106. break;
  107. case 'NO' :
  108. $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg';
  109. break;
  110. case 'CA' :
  111. $icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg';
  112. break;
  113. case 'HK' :
  114. $icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg';
  115. break;
  116. case 'SG' :
  117. $icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg';
  118. break;
  119. case 'TW' :
  120. $icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg';
  121. break;
  122. case 'TH' :
  123. $icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg';
  124. break;
  125. default :
  126. $icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' );
  127. $link = null;
  128. break;
  129. }
  130. if ( is_null( $link ) ) {
  131. $link = 'https://www.paypal.com/' . strtolower( WC()->countries->get_base_country() ) . '/webapps/mpp/paypal-popup';
  132. }
  133. if ( is_array( $icon ) ) {
  134. $icon_html = '';
  135. foreach ( $icon as $i ) {
  136. $icon_html .= '<img src="' . esc_attr( $i ) . '" alt="PayPal Acceptance Mark" />';
  137. }
  138. } else {
  139. $icon_html = '<img src="' . esc_attr( apply_filters( 'woocommerce_paypal_icon', $icon ) ) . '" alt="PayPal Acceptance Mark" />';
  140. }
  141. if ( $link ) {
  142. $what_is_paypal = sprintf( '<a href="%1$s" class="about_paypal" onclick="javascript:window.open(\'%1$s\',\'WIPaypal\',\'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=1060, height=700\'); return false;" title="' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '">' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '</a>', esc_url( $link ) );
  143. } else {
  144. $what_is_paypal = '';
  145. }
  146. return apply_filters( 'woocommerce_gateway_icon', $icon_html . $what_is_paypal, $this->id );
  147. }
  148. /**
  149. * Check if this gateway is enabled and available in the user's country
  150. *
  151. * @return bool
  152. */
  153. function is_valid_for_use() {
  154. if ( ! in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB' ) ) ) ) {
  155. return false;
  156. }
  157. return true;
  158. }
  159. /**
  160. * Admin Panel Options
  161. * - Options for bits like 'title' and availability on a country-by-country basis
  162. *
  163. * @since 1.0.0
  164. */
  165. public function admin_options() {
  166. if ( $this->is_valid_for_use() ) {
  167. parent::admin_options();
  168. } else {
  169. ?>
  170. <div class="inline error"><p><strong><?php _e( 'Gateway Disabled', 'woocommerce' ); ?></strong>: <?php _e( 'PayPal does not support your store currency.', 'woocommerce' ); ?></p></div>
  171. <?php
  172. }
  173. }
  174. /**
  175. * Initialise Gateway Settings Form Fields
  176. */
  177. public function init_form_fields() {
  178. $this->form_fields = array(
  179. 'enabled' => array(
  180. 'title' => __( 'Enable/Disable', 'woocommerce' ),
  181. 'type' => 'checkbox',
  182. 'label' => __( 'Enable PayPal standard', 'woocommerce' ),
  183. 'default' => 'yes'
  184. ),
  185. 'title' => array(
  186. 'title' => __( 'Title', 'woocommerce' ),
  187. 'type' => 'text',
  188. 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
  189. 'default' => __( 'PayPal', 'woocommerce' ),
  190. 'desc_tip' => true,
  191. ),
  192. 'description' => array(
  193. 'title' => __( 'Description', 'woocommerce' ),
  194. 'type' => 'text',
  195. 'desc_tip' => true,
  196. 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ),
  197. 'default' => __( 'Pay via PayPal; you can pay with your credit card if you don\'t have a PayPal account.', 'woocommerce' )
  198. ),
  199. 'email' => array(
  200. 'title' => __( 'PayPal Email', 'woocommerce' ),
  201. 'type' => 'email',
  202. 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ),
  203. 'default' => '',
  204. 'desc_tip' => true,
  205. 'placeholder' => 'you@youremail.com'
  206. ),
  207. 'testmode' => array(
  208. 'title' => __( 'PayPal sandbox', 'woocommerce' ),
  209. 'type' => 'checkbox',
  210. 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ),
  211. 'default' => 'no',
  212. 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account <a href="%s">here</a>.', 'woocommerce' ), 'https://developer.paypal.com/' ),
  213. ),
  214. 'debug' => array(
  215. 'title' => __( 'Debug Log', 'woocommerce' ),
  216. 'type' => 'checkbox',
  217. 'label' => __( 'Enable logging', 'woocommerce' ),
  218. 'default' => 'no',
  219. 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside <code>%s</code>', 'woocommerce' ), wc_get_log_file_path( 'paypal' ) )
  220. ),
  221. 'shipping' => array(
  222. 'title' => __( 'Shipping options', 'woocommerce' ),
  223. 'type' => 'title',
  224. 'description' => '',
  225. ),
  226. 'send_shipping' => array(
  227. 'title' => __( 'Shipping details', 'woocommerce' ),
  228. 'type' => 'checkbox',
  229. 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ),
  230. 'description' => __( 'PayPal allows us to send 1 address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing.', 'woocommerce' ),
  231. 'default' => 'no'
  232. ),
  233. 'address_override' => array(
  234. 'title' => __( 'Address override', 'woocommerce' ),
  235. 'type' => 'checkbox',
  236. 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ),
  237. 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ),
  238. 'default' => 'no'
  239. ),
  240. 'advanced' => array(
  241. 'title' => __( 'Advanced options', 'woocommerce' ),
  242. 'type' => 'title',
  243. 'description' => '',
  244. ),
  245. 'receiver_email' => array(
  246. 'title' => __( 'Receiver Email', 'woocommerce' ),
  247. 'type' => 'email',
  248. 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ),
  249. 'default' => '',
  250. 'desc_tip' => true,
  251. 'placeholder' => 'you@youremail.com'
  252. ),
  253. 'invoice_prefix' => array(
  254. 'title' => __( 'Invoice Prefix', 'woocommerce' ),
  255. 'type' => 'text',
  256. 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ),
  257. 'default' => 'WC-',
  258. 'desc_tip' => true,
  259. ),
  260. 'paymentaction' => array(
  261. 'title' => __( 'Payment Action', 'woocommerce' ),
  262. 'type' => 'select',
  263. 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ),
  264. 'default' => 'sale',
  265. 'desc_tip' => true,
  266. 'options' => array(
  267. 'sale' => __( 'Capture', 'woocommerce' ),
  268. 'authorization' => __( 'Authorize', 'woocommerce' )
  269. )
  270. ),
  271. 'page_style' => array(
  272. 'title' => __( 'Page Style', 'woocommerce' ),
  273. 'type' => 'text',
  274. 'description' => __( 'Optionally enter the name of the page style you wish to use. These are defined within your PayPal account.', 'woocommerce' ),
  275. 'default' => '',
  276. 'desc_tip' => true,
  277. 'placeholder' => __( 'Optional', 'woocommerce' )
  278. ),
  279. 'identity_token' => array(
  280. 'title' => __( 'PayPal Identity Token', 'woocommerce' ),
  281. 'type' => 'text',
  282. 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Website Payment Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ),
  283. 'default' => '',
  284. 'desc_tip' => true,
  285. 'placeholder' => __( 'Optional', 'woocommerce' )
  286. ),
  287. 'api_details' => array(
  288. 'title' => __( 'API Credentials', 'woocommerce' ),
  289. 'type' => 'title',
  290. 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your PayPal API Credentials %shere%s.', 'woocommerce' ), '<a href="https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#creating-classic-api-credentials">', '</a>' ),
  291. ),
  292. 'api_username' => array(
  293. 'title' => __( 'API Username', 'woocommerce' ),
  294. 'type' => 'text',
  295. 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
  296. 'default' => '',
  297. 'desc_tip' => true,
  298. 'placeholder' => __( 'Optional', 'woocommerce' )
  299. ),
  300. 'api_password' => array(
  301. 'title' => __( 'API Password', 'woocommerce' ),
  302. 'type' => 'text',
  303. 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
  304. 'default' => '',
  305. 'desc_tip' => true,
  306. 'placeholder' => __( 'Optional', 'woocommerce' )
  307. ),
  308. 'api_signature' => array(
  309. 'title' => __( 'API Signature', 'woocommerce' ),
  310. 'type' => 'text',
  311. 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
  312. 'default' => '',
  313. 'desc_tip' => true,
  314. 'placeholder' => __( 'Optional', 'woocommerce' )
  315. ),
  316. );
  317. }
  318. /**
  319. * Limit the length of item names
  320. * @param string $item_name
  321. * @return string
  322. */
  323. public function paypal_item_name( $item_name ) {
  324. if ( strlen( $item_name ) > 127 ) {
  325. $item_name = substr( $item_name, 0, 124 ) . '...';
  326. }
  327. return html_entity_decode( $item_name, ENT_NOQUOTES, 'UTF-8' );
  328. }
  329. /**
  330. * Get PayPal Args for passing to PP
  331. *
  332. * @param WC_Order $order
  333. * @return array
  334. */
  335. function get_paypal_args( $order ) {
  336. $order_id = $order->id;
  337. if ( 'yes' == $this->debug ) {
  338. $this->log->add( 'paypal', 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url );
  339. }
  340. if ( in_array( $order->billing_country, array( 'US','CA' ) ) ) {
  341. $order->billing_phone = str_replace( array( '(', '-', ' ', ')', '.' ), '', $order->billing_phone );
  342. $phone_args = array(
  343. 'night_phone_a' => substr( $order->billing_phone, 0, 3 ),
  344. 'night_phone_b' => substr( $order->billing_phone, 3, 3 ),
  345. 'night_phone_c' => substr( $order->billing_phone, 6, 4 ),
  346. 'day_phone_a' => substr( $order->billing_phone, 0, 3 ),
  347. 'day_phone_b' => substr( $order->billing_phone, 3, 3 ),
  348. 'day_phone_c' => substr( $order->billing_phone, 6, 4 )
  349. );
  350. } else {
  351. $phone_args = array(
  352. 'night_phone_b' => $order->billing_phone,
  353. 'day_phone_b' => $order->billing_phone
  354. );
  355. }
  356. // PayPal Args
  357. $paypal_args = array_merge(
  358. array(
  359. 'cmd' => '_cart',
  360. 'business' => $this->email,
  361. 'no_note' => 1,
  362. 'currency_code' => get_woocommerce_currency(),
  363. 'charset' => 'utf-8',
  364. 'rm' => is_ssl() ? 2 : 1,
  365. 'upload' => 1,
  366. 'return' => esc_url( add_query_arg( 'utm_nooverride', '1', $this->get_return_url( $order ) ) ),
  367. 'cancel_return' => esc_url( $order->get_cancel_order_url() ),
  368. 'page_style' => $this->page_style,
  369. 'paymentaction' => $this->paymentaction,
  370. 'bn' => 'WooThemes_Cart',
  371. // Order key + ID
  372. 'invoice' => $this->invoice_prefix . ltrim( $order->get_order_number(), '#' ),
  373. 'custom' => serialize( array( $order_id, $order->order_key ) ),
  374. // IPN
  375. 'notify_url' => $this->notify_url,
  376. // Billing Address info
  377. 'first_name' => $order->billing_first_name,
  378. 'last_name' => $order->billing_last_name,
  379. 'company' => $order->billing_company,
  380. 'address1' => $order->billing_address_1,
  381. 'address2' => $order->billing_address_2,
  382. 'city' => $order->billing_city,
  383. 'state' => $this->get_paypal_state( $order->billing_country, $order->billing_state ),
  384. 'zip' => $order->billing_postcode,
  385. 'country' => $order->billing_country,
  386. 'email' => $order->billing_email
  387. ),
  388. $phone_args
  389. );
  390. // Shipping
  391. if ( 'yes' == $this->send_shipping ) {
  392. $paypal_args['address_override'] = ( $this->address_override == 'yes' ) ? 1 : 0;
  393. $paypal_args['no_shipping'] = 0;
  394. // If we are sending shipping, send shipping address instead of billing
  395. $paypal_args['first_name'] = $order->shipping_first_name;
  396. $paypal_args['last_name'] = $order->shipping_last_name;
  397. $paypal_args['company'] = $order->shipping_company;
  398. $paypal_args['address1'] = $order->shipping_address_1;
  399. $paypal_args['address2'] = $order->shipping_address_2;
  400. $paypal_args['city'] = $order->shipping_city;
  401. $paypal_args['state'] = $this->get_paypal_state( $order->shipping_country, $order->shipping_state );
  402. $paypal_args['country'] = $order->shipping_country;
  403. $paypal_args['zip'] = $order->shipping_postcode;
  404. } else {
  405. $paypal_args['no_shipping'] = 1;
  406. }
  407. // Try to send line items, or default to sending the order as a whole
  408. if ( $line_items = $this->get_line_items( $order ) ) {
  409. $paypal_args = array_merge( $paypal_args, $line_items );
  410. } else {
  411. // Don't pass items - paypal borks tax due to prices including tax. PayPal has no option for tax inclusive pricing sadly. Pass 1 item for the order items overall
  412. $item_names = array();
  413. if ( sizeof( $order->get_items() ) > 0 ) {
  414. foreach ( $order->get_items() as $item ) {
  415. if ( $item['qty'] ) {
  416. $item_names[] = $item['name'] . ' x ' . $item['qty'];
  417. }
  418. }
  419. }
  420. $paypal_args['item_name_1'] = $this->paypal_item_name( sprintf( __( 'Order %s' , 'woocommerce'), $order->get_order_number() ) . " - " . implode( ', ', $item_names ) );
  421. $paypal_args['quantity_1'] = '1';
  422. $paypal_args['amount_1'] = number_format( $order->get_total() - round( $order->get_total_shipping() + $order->get_shipping_tax(), 2 ) + $order->get_order_discount(), 2, '.', '' );
  423. // Shipping Cost
  424. // No longer using shipping_1 because
  425. // a) paypal ignore it if *any* shipping rules are within paypal
  426. // b) paypal ignore anything over 5 digits, so 999.99 is the max
  427. if ( ( $order->get_total_shipping() + $order->get_shipping_tax() ) > 0 ) {
  428. $paypal_args['item_name_2'] = $this->paypal_item_name( __( 'Shipping via', 'woocommerce' ) . ' ' . ucwords( $order->get_shipping_method() ) );
  429. $paypal_args['quantity_2'] = '1';
  430. $paypal_args['amount_2'] = number_format( $order->get_total_shipping() + $order->get_shipping_tax(), 2, '.', '' );
  431. }
  432. // Discount
  433. if ( $order->get_order_discount() ) {
  434. $paypal_args['discount_amount_cart'] = $order->get_order_discount();
  435. }
  436. }
  437. $paypal_args = apply_filters( 'woocommerce_paypal_args', $paypal_args );
  438. return $paypal_args;
  439. }
  440. /**
  441. * Get line items to send to paypal
  442. *
  443. * @param WC_Order $order
  444. * @return array on success, or false when it is not possible to send line items
  445. */
  446. private function get_line_items( $order ) {
  447. // Do not send lines for tax inclusive prices
  448. if ( 'yes' === get_option( 'woocommerce_calc_taxes' ) && 'yes' === get_option( 'woocommerce_prices_include_tax' ) ) {
  449. return false;
  450. }
  451. // Do not send lines when order discount is present, or too many line items in the order.
  452. if ( $order->get_order_discount() > 0 || ( sizeof( $order->get_items() ) + sizeof( $order->get_fees() ) ) >= 9 ) {
  453. return false;
  454. }
  455. $item_loop = 0;
  456. $args = array();
  457. $args['tax_cart'] = $order->get_total_tax();
  458. // Products
  459. if ( sizeof( $order->get_items() ) > 0 ) {
  460. foreach ( $order->get_items() as $item ) {
  461. if ( ! $item['qty'] ) {
  462. continue;
  463. }
  464. $item_loop ++;
  465. $product = $order->get_product_from_item( $item );
  466. $item_name = $item['name'];
  467. $item_meta = new WC_Order_Item_Meta( $item['item_meta'] );
  468. if ( $meta = $item_meta->display( true, true ) ) {
  469. $item_name .= ' ( ' . $meta . ' )';
  470. }
  471. $args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( $item_name );
  472. $args[ 'quantity_' . $item_loop ] = $item['qty'];
  473. $args[ 'amount_' . $item_loop ] = $order->get_item_subtotal( $item, false );
  474. if ( $args[ 'amount_' . $item_loop ] < 0 ) {
  475. return false; // Abort - negative line
  476. }
  477. if ( $product->get_sku() ) {
  478. $args[ 'item_number_' . $item_loop ] = $product->get_sku();
  479. }
  480. }
  481. }
  482. // Discount
  483. if ( $order->get_cart_discount() > 0 ) {
  484. $args['discount_amount_cart'] = round( $order->get_cart_discount(), 2 );
  485. }
  486. // Fees
  487. if ( sizeof( $order->get_fees() ) > 0 ) {
  488. foreach ( $order->get_fees() as $item ) {
  489. $item_loop ++;
  490. $args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( $item['name'] );
  491. $args[ 'quantity_' . $item_loop ] = 1;
  492. $args[ 'amount_' . $item_loop ] = $item['line_total'];
  493. if ( $args[ 'amount_' . $item_loop ] < 0 ) {
  494. return false; // Abort - negative line
  495. }
  496. }
  497. }
  498. // Shipping Cost item - paypal only allows shipping per item, we want to send shipping for the order
  499. if ( $order->get_total_shipping() > 0 ) {
  500. $item_loop ++;
  501. $args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ) );
  502. $args[ 'quantity_' . $item_loop ] = '1';
  503. $args[ 'amount_' . $item_loop ] = number_format( $order->get_total_shipping(), 2, '.', '' );
  504. }
  505. return $args;
  506. }
  507. /**
  508. * Process the payment and return the result
  509. *
  510. * @param int $order_id
  511. * @return array
  512. */
  513. public function process_payment( $order_id ) {
  514. $order = wc_get_order( $order_id );
  515. $paypal_args = $this->get_paypal_args( $order );
  516. $paypal_args = http_build_query( $paypal_args, '', '&' );
  517. if ( 'yes' == $this->testmode ) {
  518. $paypal_adr = $this->testurl . '?test_ipn=1&';
  519. } else {
  520. $paypal_adr = $this->liveurl . '?';
  521. }
  522. return array(
  523. 'result' => 'success',
  524. 'redirect' => $paypal_adr . $paypal_args
  525. );
  526. }
  527. /**
  528. * Process a refund if supported
  529. * @param int $order_id
  530. * @param float $amount
  531. * @param string $reason
  532. * @return bool|wp_error True or false based on success, or a WP_Error object
  533. */
  534. public function process_refund( $order_id, $amount = null, $reason = '' ) {
  535. $order = wc_get_order( $order_id );
  536. if ( ! $order || ! $order->get_transaction_id() || ! $this->api_username || ! $this->api_password || ! $this->api_signature ) {
  537. return false;
  538. }
  539. $post_data = array(
  540. 'VERSION' => '84.0',
  541. 'SIGNATURE' => $this->api_signature,
  542. 'USER' => $this->api_username,
  543. 'PWD' => $this->api_password,
  544. 'METHOD' => 'RefundTransaction',
  545. 'TRANSACTIONID' => $order->get_transaction_id(),
  546. 'REFUNDTYPE' => is_null( $amount ) ? 'Full' : 'Partial'
  547. );
  548. if ( ! is_null( $amount ) ) {
  549. $post_data['AMT'] = number_format( $amount, 2, '.', '' );
  550. $post_data['CURRENCYCODE'] = $order->get_order_currency();
  551. }
  552. if ( $reason ) {
  553. if ( 255 < strlen( $reason ) ) {
  554. $reason = substr( $reason, 0, 252 ) . '...';
  555. }
  556. $post_data['NOTE'] = html_entity_decode( $reason, ENT_NOQUOTES, 'UTF-8' );
  557. }
  558. $response = wp_remote_post( 'yes' === $this->testmode ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp', array(
  559. 'method' => 'POST',
  560. 'body' => $post_data,
  561. 'timeout' => 70,
  562. 'sslverify' => false,
  563. 'user-agent' => 'WooCommerce',
  564. 'httpversion' => '1.1'
  565. )
  566. );
  567. if ( is_wp_error( $response ) ) {
  568. return $response;
  569. }
  570. if ( empty( $response['body'] ) ) {
  571. return new WP_Error( 'paypal-error', __( 'Empty Paypal response.', 'woocommerce' ) );
  572. }
  573. parse_str( $response['body'], $parsed_response );
  574. switch ( strtolower( $parsed_response['ACK'] ) ) {
  575. case 'success':
  576. case 'successwithwarning':
  577. $order->add_order_note( sprintf( __( 'Refunded %s - Refund ID: %s', 'woocommerce' ), $parsed_response['GROSSREFUNDAMT'], $parsed_response['REFUNDTRANSACTIONID'] ) );
  578. return true;
  579. break;
  580. }
  581. return false;
  582. }
  583. /**
  584. * Check PayPal IPN validity
  585. **/
  586. public function check_ipn_request_is_valid( $ipn_response ) {
  587. // Get url
  588. if ( 'yes' == $this->testmode ) {
  589. $paypal_adr = $this->testurl;
  590. } else {
  591. $paypal_adr = $this->liveurl;
  592. }
  593. if ( 'yes' == $this->debug ) {
  594. $this->log->add( 'paypal', 'Checking IPN response is valid via ' . $paypal_adr . '...' );
  595. }
  596. // Get received values from post data
  597. $validate_ipn = array( 'cmd' => '_notify-validate' );
  598. $validate_ipn += stripslashes_deep( $ipn_response );
  599. // Send back post vars to paypal
  600. $params = array(
  601. 'body' => $validate_ipn,
  602. 'sslverify' => false,
  603. 'timeout' => 60,
  604. 'httpversion' => '1.1',
  605. 'compress' => false,
  606. 'decompress' => false,
  607. 'user-agent' => 'WooCommerce/' . WC()->version
  608. );
  609. if ( 'yes' == $this->debug ) {
  610. $this->log->add( 'paypal', 'IPN Request: ' . print_r( $params, true ) );
  611. }
  612. // Post back to get a response
  613. $response = wp_remote_post( $paypal_adr, $params );
  614. if ( 'yes' == $this->debug ) {
  615. $this->log->add( 'paypal', 'IPN Response: ' . print_r( $response, true ) );
  616. }
  617. // check to see if the request was valid
  618. if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
  619. if ( 'yes' == $this->debug ) {
  620. $this->log->add( 'paypal', 'Received valid response from PayPal' );
  621. }
  622. return true;
  623. }
  624. if ( 'yes' == $this->debug ) {
  625. $this->log->add( 'paypal', 'Received invalid response from PayPal' );
  626. if ( is_wp_error( $response ) ) {
  627. $this->log->add( 'paypal', 'Error response: ' . $response->get_error_message() );
  628. }
  629. }
  630. return false;
  631. }
  632. /**
  633. * Check for PayPal IPN Response
  634. */
  635. public function check_ipn_response() {
  636. @ob_clean();
  637. $ipn_response = ! empty( $_POST ) ? $_POST : false;
  638. if ( $ipn_response && $this->check_ipn_request_is_valid( $ipn_response ) ) {
  639. header( 'HTTP/1.1 200 OK' );
  640. do_action( "valid-paypal-standard-ipn-request", $ipn_response );
  641. } else {
  642. wp_die( "PayPal IPN Request Failure", "PayPal IPN", array( 'response' => 200 ) );
  643. }
  644. }
  645. /**
  646. * Successful Payment!
  647. *
  648. * @param array $posted
  649. */
  650. public function successful_request( $posted ) {
  651. $posted = stripslashes_deep( $posted );
  652. // Custom holds post ID
  653. if ( ! empty( $posted['invoice'] ) && ! empty( $posted['custom'] ) ) {
  654. $order = $this->get_paypal_order( $posted['custom'], $posted['invoice'] );
  655. if ( 'yes' == $this->debug ) {
  656. $this->log->add( 'paypal', 'Found order #' . $order->id );
  657. }
  658. // Lowercase returned variables
  659. $posted['payment_status'] = strtolower( $posted['payment_status'] );
  660. $posted['txn_type'] = strtolower( $posted['txn_type'] );
  661. // Sandbox fix
  662. if ( 1 == $posted['test_ipn'] && 'pending' == $posted['payment_status'] ) {
  663. $posted['payment_status'] = 'completed';
  664. }
  665. if ( 'yes' == $this->debug ) {
  666. $this->log->add( 'paypal', 'Payment status: ' . $posted['payment_status'] );
  667. }
  668. // We are here so lets check status and do actions
  669. switch ( $posted['payment_status'] ) {
  670. case 'completed' :
  671. case 'pending' :
  672. // Check order not already completed
  673. if ( $order->has_status( 'completed' ) ) {
  674. if ( 'yes' == $this->debug ) {
  675. $this->log->add( 'paypal', 'Aborting, Order #' . $order->id . ' is already complete.' );
  676. }
  677. exit;
  678. }
  679. // Check valid txn_type
  680. $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money' );
  681. if ( ! in_array( $posted['txn_type'], $accepted_types ) ) {
  682. if ( 'yes' == $this->debug ) {
  683. $this->log->add( 'paypal', 'Aborting, Invalid type:' . $posted['txn_type'] );
  684. }
  685. exit;
  686. }
  687. // Validate currency
  688. if ( $order->get_order_currency() != $posted['mc_currency'] ) {
  689. if ( 'yes' == $this->debug ) {
  690. $this->log->add( 'paypal', 'Payment error: Currencies do not match (sent "' . $order->get_order_currency() . '" | returned "' . $posted['mc_currency'] . '")' );
  691. }
  692. // Put this order on-hold for manual checking
  693. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $posted['mc_currency'] ) );
  694. exit;
  695. }
  696. // Validate amount
  697. if ( $order->get_total() != $posted['mc_gross'] ) {
  698. if ( 'yes' == $this->debug ) {
  699. $this->log->add( 'paypal', 'Payment error: Amounts do not match (gross ' . $posted['mc_gross'] . ')' );
  700. }
  701. // Put this order on-hold for manual checking
  702. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $posted['mc_gross'] ) );
  703. exit;
  704. }
  705. // Validate Email Address
  706. if ( strcasecmp( trim( $posted['receiver_email'] ), trim( $this->receiver_email ) ) != 0 ) {
  707. if ( 'yes' == $this->debug ) {
  708. $this->log->add( 'paypal', "IPN Response is for another one: {$posted['receiver_email']} our email is {$this->receiver_email}" );
  709. }
  710. // Put this order on-hold for manual checking
  711. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $posted['receiver_email'] ) );
  712. exit;
  713. }
  714. // Store PP Details
  715. if ( ! empty( $posted['payer_email'] ) ) {
  716. update_post_meta( $order->id, 'Payer PayPal address', wc_clean( $posted['payer_email'] ) );
  717. }
  718. if ( ! empty( $posted['first_name'] ) ) {
  719. update_post_meta( $order->id, 'Payer first name', wc_clean( $posted['first_name'] ) );
  720. }
  721. if ( ! empty( $posted['last_name'] ) ) {
  722. update_post_meta( $order->id, 'Payer last name', wc_clean( $posted['last_name'] ) );
  723. }
  724. if ( ! empty( $posted['payment_type'] ) ) {
  725. update_post_meta( $order->id, 'Payment type', wc_clean( $posted['payment_type'] ) );
  726. }
  727. if ( $posted['payment_status'] == 'completed' ) {
  728. $order->add_order_note( __( 'IPN payment completed', 'woocommerce' ) );
  729. $txn_id = ( ! empty( $posted['txn_id'] ) ) ? wc_clean( $posted['txn_id'] ) : '';
  730. $order->payment_complete( $txn_id );
  731. } else {
  732. $order->update_status( 'on-hold', sprintf( __( 'Payment pending: %s', 'woocommerce' ), $posted['pending_reason'] ) );
  733. }
  734. if ( 'yes' == $this->debug ) {
  735. $this->log->add( 'paypal', 'Payment complete.' );
  736. }
  737. break;
  738. case 'denied' :
  739. case 'expired' :
  740. case 'failed' :
  741. case 'voided' :
  742. // Order failed
  743. $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) );
  744. break;
  745. case 'refunded' :
  746. // Only handle full refunds, not partial
  747. if ( $order->get_total() == ( $posted['mc_gross'] * -1 ) ) {
  748. // Mark order as refunded
  749. $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) );
  750. $this->send_ipn_email_notification(
  751. sprintf( __( 'Payment for order %s refunded/reversed', 'woocommerce' ), $order->get_order_number() ),
  752. sprintf( __( 'Order %s has been marked as refunded - PayPal reason code: %s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] )
  753. );
  754. }
  755. break;
  756. case 'reversed' :
  757. // Mark order as refunded
  758. $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) );
  759. $this->send_ipn_email_notification(
  760. sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), $order->get_order_number() ),
  761. sprintf(__( 'Order %s has been marked on-hold due to a reversal - PayPal reason code: %s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] )
  762. );
  763. break;
  764. case 'canceled_reversal' :
  765. $this->send_ipn_email_notification(
  766. sprintf( __( 'Reversal cancelled for order %s', 'woocommerce' ), $order->get_order_number() ),
  767. sprintf( __( 'Order %s has had a reversal cancelled. Please check the status of payment and update the order status accordingly.', 'woocommerce' ), $order->get_order_number() )
  768. );
  769. break;
  770. default :
  771. // No action
  772. break;
  773. }
  774. exit;
  775. }
  776. }
  777. /**
  778. * Send a notification to the user handling orders.
  779. * @param string $subject
  780. * @param string $message
  781. */
  782. public function send_ipn_email_notification( $subject, $message ) {
  783. $new_order_settings = get_option( 'woocommerce_new_order_settings', array() );
  784. $mailer = WC()->mailer();
  785. $message = $mailer->wrap_message( $subject, $message );
  786. $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), $subject, $message );
  787. }
  788. /**
  789. * Return handler
  790. *
  791. * Alternative to IPN
  792. */
  793. public function pdt_return_handler() {
  794. $posted = stripslashes_deep( $_REQUEST );
  795. if ( ! empty( $this->identity_token ) && ! empty( $posted['cm'] ) ) {
  796. $order = $this->get_paypal_order( $posted['cm'] );
  797. if ( ! $order->has_status( 'pending' ) ) {
  798. return false;
  799. }
  800. $posted['st'] = strtolower( $posted['st'] );
  801. switch ( $posted['st'] ) {
  802. case 'completed' :
  803. // Validate transaction
  804. if ( 'yes' == $this->testmode ) {
  805. $paypal_adr = $this->testurl;
  806. } else {
  807. $paypal_adr = $this->liveurl;
  808. }
  809. $pdt = array(
  810. 'body' => array(
  811. 'cmd' => '_notify-synch',
  812. 'tx' => $posted['tx'],
  813. 'at' => $this->identity_token
  814. ),
  815. 'sslverify' => false,
  816. 'timeout' => 60,
  817. 'httpversion' => '1.1',
  818. 'user-agent' => 'WooCommerce/' . WC_VERSION
  819. );
  820. // Post back to get a response
  821. $response = wp_remote_post( $paypal_adr, $pdt );
  822. if ( is_wp_error( $response ) ) {
  823. return false;
  824. }
  825. if ( ! strpos( $response['body'], "SUCCESS" ) === 0 ) {
  826. return false;
  827. }
  828. // Validate Amount
  829. if ( $order->get_total() != $posted['amt'] ) {
  830. if ( 'yes' == $this->debug ) {
  831. $this->log->add( 'paypal', 'Payment error: Amounts do not match (amt ' . $posted['amt'] . ')' );
  832. }
  833. // Put this order on-hold for manual checking
  834. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $posted['amt'] ) );
  835. return true;
  836. } else {
  837. // Store PP Details
  838. $order->add_order_note( __( 'PDT payment completed', 'woocommerce' ) );
  839. $txn_id = ( ! empty( $posted['tx'] ) ) ? wc_clean( $posted['tx'] ) : '';
  840. $order->payment_complete( $txn_id );
  841. return true;
  842. }
  843. break;
  844. }
  845. }
  846. return false;
  847. }
  848. /**
  849. * get_paypal_order function.
  850. *
  851. * @param string $custom
  852. * @param string $invoice
  853. * @return WC_Order object
  854. */
  855. private function get_paypal_order( $custom, $invoice = '' ) {
  856. $custom = maybe_unserialize( $custom );
  857. // Backwards comp for IPN requests
  858. if ( is_numeric( $custom ) ) {
  859. $order_id = (int) $custom;
  860. $order_key = $invoice;
  861. } elseif( is_string( $custom ) ) {
  862. $order_id = (int) str_replace( $this->invoice_prefix, '', $custom );
  863. $order_key = $custom;
  864. } else {
  865. list( $order_id, $order_key ) = $custom;
  866. }
  867. $order = wc_get_order( $order_id );
  868. if ( ! isset( $order->id ) ) {
  869. // We have an invalid $order_id, probably because invoice_prefix has changed
  870. $order_id = wc_get_order_id_by_order_key( $order_key );
  871. $order = wc_get_order( $order_id );
  872. }
  873. // Validate key
  874. if ( $order->order_key !== $order_key ) {
  875. if ( 'yes' == $this->debug ) {
  876. $this->log->add( 'paypal', 'Error: Order Key does not match invoice.' );
  877. }
  878. exit;
  879. }
  880. return $order;
  881. }
  882. /**
  883. * Get the state to send to paypal
  884. * @param string $cc
  885. * @param string $state
  886. * @return string
  887. */
  888. public function get_paypal_state( $cc, $state ) {
  889. if ( 'US' === $cc ) {
  890. return $state;
  891. }
  892. $states = WC()->countries->get_states( $cc );
  893. if ( isset( $states[ $state ] ) ) {
  894. return $states[ $state ];
  895. }
  896. return $state;
  897. }
  898. /**
  899. * Get the transaction URL.
  900. *
  901. * @param WC_Order $order
  902. *
  903. * @return string
  904. */
  905. public function get_transaction_url( $order ) {
  906. if ( 'yes' == $this->testmode ) {
  907. $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
  908. } else {
  909. $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
  910. }
  911. return parent::get_transaction_url( $order );
  912. }
  913. }