MS_Gateway_Paypalstandard

Gateway: Paypal Standard.

Defined (1)

The class is defined in the following location(s).

/app/gateway/paypalstandard/class-ms-gateway-paypalstandard.php  
  1. class MS_Gateway_Paypalstandard extends MS_Gateway { 
  2.  
  3. const ID = 'paypalstandard'; 
  4.  
  5. /** 
  6. * Gateway singleton instance. 
  7. * @since 1.0.0 
  8. * @var string $instance 
  9. */ 
  10. public static $instance; 
  11.  
  12. /** 
  13. * Paypal merchant ID. 
  14. * @since 1.0.0 
  15. * @var bool $merchant_id 
  16. */ 
  17. protected $merchant_id; 
  18.  
  19. /** 
  20. * Paypal country site. 
  21. * @since 1.0.0 
  22. * @var bool $paypal_site 
  23. */ 
  24. protected $paypal_site; 
  25.  
  26. /** 
  27. * Hook to add custom transaction status. 
  28. * @since 1.0.0 
  29. */ 
  30. public function after_load() { 
  31. parent::after_load(); 
  32.  
  33. $this->id = self::ID; 
  34. $this->name = __( 'PayPal Standard Gateway', 'membership2' ); 
  35. $this->group = 'PayPal'; 
  36. $this->manual_payment = false; // Recurring charged automatically 
  37. $this->pro_rate = false; 
  38.  
  39. if ( $this->active && $this->is_live_mode() && strpos( $this->merchant_id, '@' ) ) { 
  40. $settings_url = MS_Controller_Plugin::get_admin_url( 
  41. 'settings',  
  42. array( 'tab' => MS_Controller_Settings::TAB_PAYMENT ) 
  43. ); 
  44. lib3()->ui->admin_message( 
  45. sprintf( 
  46. __( 'Warning: You use your email address for the PayPal Standard gateway instead of your Merchant ID. Please check %syour payment settings%s and enter the Merchant ID instead', 'membership2' ),  
  47. '<a href="' . $settings_url . '">',  
  48. '</a>' 
  49. ),  
  50. 'err' 
  51. ); 
  52.  
  53. /** 
  54. * Processes gateway IPN return. 
  55. * @since 1.0.0 
  56. * @param MS_Model_Transactionlog $log Optional. A transaction log item 
  57. * that will be updated instead of creating a new log entry. 
  58. */ 
  59. public function handle_return( $log = false ) { 
  60. $success = false; 
  61. $ignore = false; 
  62. $exit = false; 
  63. $redirect = false; 
  64. $notes = ''; 
  65. $status = null; 
  66. $notes_pay = ''; 
  67. $notes_txn = ''; 
  68. $external_id = null; 
  69. $invoice_id = 0; 
  70. $subscription_id = 0; 
  71. $amount = 0; 
  72. $transaction_type = ''; 
  73. $payment_status = ''; 
  74. $ext_type = false; 
  75.  
  76. if ( ! empty( $_POST[ 'txn_type'] ) ) { 
  77. $transaction_type = strtolower( $_POST[ 'txn_type'] ); 
  78. if ( isset( $_POST['mc_gross'] ) ) { 
  79. $amount = (float) $_POST['mc_gross']; 
  80. } elseif ( isset( $_POST['mc_amount3'] ) ) { 
  81. // mc_amount1 and mc_amount2 are for trial period prices. 
  82. $amount = (float) $_POST['mc_amount3']; 
  83. if ( ! empty( $_POST[ 'payment_status'] ) ) { 
  84. $payment_status = strtolower( $_POST[ 'payment_status'] ); 
  85. if ( ! empty( $_POST['txn_id'] ) ) { 
  86. $external_id = $_POST['txn_id']; 
  87. if ( ! empty( $_POST['mc_currency'] ) ) { 
  88. $currency = $_POST['mc_currency']; 
  89.  
  90. // Step 1: Find the invoice_id and determine if payment is M2 or M1. 
  91. if ( $payment_status || $transaction_type ) { 
  92. if ( ! empty( $_POST['invoice'] ) ) { 
  93. // BEST CASE: 
  94. // 'invoice' is set in all regular M2 subscriptions! 
  95. $invoice_id = intval( $_POST['invoice'] ); 
  96.  
  97. /** 
  98. * PayPal only knows the first invoice of the subscription. 
  99. * So we need to check: If the invoice is already paid then the 
  100. * payment is for a follow-up invoice. 
  101. */ 
  102. $invoice = MS_Factory::load( 'MS_Model_Invoice', $invoice_id ); 
  103. if ( $invoice->is_paid() ) { 
  104. $subscription = $invoice->get_subscription(); 
  105. $invoice_id = $subscription->first_unpaid_invoice(); 
  106. } elseif ( ! empty( $_POST['custom'] ) ) { 
  107. // FALLBACK A: 
  108. // Maybe it's an imported M1 subscription. 
  109. $infos = explode( ':', $_POST['custom'] ); 
  110. if ( count( $infos ) > 2 ) { 
  111. // $infos should contain [timestamp, user_id, sub_id, key] 
  112.  
  113. $m1_user_id = intval( $infos[1] ); 
  114. $m1_sub_id = intval( $infos[2] ); // Roughtly equals M2 membership->id. 
  115.  
  116. // M1 payments use the following type/status values. 
  117. $pay_types = array( 'subscr_signup', 'subscr_payment' ); 
  118. $pay_stati = array( 'completed', 'processed' ); 
  119.  
  120. if ( $m1_user_id > 0 && $m1_sub_id > 0 ) { 
  121. if ( in_array( $transaction_type, $pay_types ) ) { 
  122. $ext_type = 'm1'; 
  123. } elseif ( in_array( $payment_status, $pay_stati ) ) { 
  124. $ext_type = 'm1'; 
  125.  
  126. if ( 'm1' == $ext_type ) { 
  127. $is_linked = false; 
  128.  
  129. // Seems to be a valid M1 payment: 
  130. // Find the associated imported subscription! 
  131. $subscription = MS_Model_Import::find_subscription( 
  132. $m1_user_id,  
  133. $m1_sub_id,  
  134. 'source',  
  135. self::ID 
  136. ); 
  137.  
  138. if ( ! $subscription ) { 
  139. $membership = MS_Model_Import::membership_by_source( 
  140. $m1_sub_id 
  141. ); 
  142.  
  143. if ( $membership ) { 
  144. $is_linked = true; 
  145. $notes = sprintf( 
  146. 'Error: User is not subscribed to Membership %s.',  
  147. $membership->id 
  148. ); 
  149.  
  150. $invoice_id = $subscription->first_unpaid_invoice(); 
  151.  
  152. if ( ! $is_linked && ! $invoice_id ) { 
  153. MS_Model_Import::need_matching( $m1_sub_id, 'm1' ); 
  154. // end if: 'm1' == $ext_type 
  155. } elseif ( ! empty( $_POST['btn_id'] ) && ! empty( $_POST['payer_email'] ) ) { 
  156. // FALLBACK B: 
  157. // Payment was made by a custom PayPal Payment button. 
  158. $user = get_user_by( 'email', $_POST['payer_email'] ); 
  159.  
  160. if ( $user && $user->ID ) { 
  161. $ext_type = 'pay_btn'; 
  162. $is_linked = false; 
  163.  
  164. $subscription = MS_Model_Import::find_subscription( 
  165. $user->ID,  
  166. $_POST['btn_id'],  
  167. 'pay_btn',  
  168. self::ID 
  169. ); 
  170.  
  171. if ( ! $subscription ) { 
  172. $membership = MS_Model_Import::membership_by_matching( 
  173. 'pay_btn',  
  174. $_POST['btn_id'] 
  175. ); 
  176.  
  177. if ( $membership ) { 
  178. $is_linked = true; 
  179. $notes = sprintf( 
  180. 'Error: User is not subscribed to Membership %s.',  
  181. $membership->id 
  182. ); 
  183.  
  184. $invoice_id = $subscription->first_unpaid_invoice(); 
  185.  
  186. if ( ! $is_linked && ! $invoice_id ) { 
  187. MS_Model_Import::need_matching( $_POST['btn_id'], 'pay_btn' ); 
  188. } else { 
  189. $notes = sprintf( 
  190. 'Error: Could not find user "%s".',  
  191. $_POST['payer_email'] 
  192. ); 
  193. // end if: 'pay_btn' == $ext_type 
  194.  
  195. // Step 2a: Check if the txn_id was already processed by M2. 
  196. if ( MS_Model_Transactionlog::was_processed( self::ID, $external_id ) ) { 
  197. $notes = 'Duplicate: Already processed that transaction.'; 
  198. $success = false; 
  199. $ignore = true; 
  200.  
  201. // Step 2b: If we have an invoice_id then process the payment. 
  202. elseif ( $invoice_id ) { 
  203. if ( $this->is_live_mode() ) { 
  204. $domain = 'https://www.paypal.com'; 
  205. } else { 
  206. $domain = 'https://www.sandbox.paypal.com'; 
  207.  
  208. // PayPal post authenticity verification. 
  209. $ipn_data = (array) stripslashes_deep( $_POST ); 
  210. $ipn_data['cmd'] = '_notify-validate'; 
  211. $response = wp_remote_post( 
  212. $domain . '/cgi-bin/webscr',  
  213. array( 
  214. 'timeout' => 60,  
  215. 'sslverify' => false,  
  216. 'httpversion' => '1.1',  
  217. 'body' => $ipn_data,  
  218. ); 
  219.  
  220. $invoice = MS_Factory::load( 'MS_Model_Invoice', $invoice_id ); 
  221.  
  222. if ( ! is_wp_error( $response ) 
  223. && 200 == $response['response']['code'] 
  224. && ! empty( $response['body'] ) 
  225. && 'VERIFIED' == $response['body'] 
  226. && $invoice->id == $invoice_id 
  227. ) { 
  228. $subscription = $invoice->get_subscription(); 
  229. $membership = $subscription->get_membership(); 
  230. $member = $subscription->get_member(); 
  231. $subscription_id = $subscription->id; 
  232.  
  233. // Process PayPal payment status 
  234. if ( $payment_status ) { 
  235. switch ( $payment_status ) { 
  236. // Successful payment 
  237. case 'completed': 
  238. case 'processed': 
  239. $success = true; 
  240. if ( $amount == $invoice->total ) { 
  241. $notes .= __( 'Payment successful', 'membership2' ); 
  242. } else { 
  243. $notes .= __( 'Payment registered, though amount differs from invoice.', 'membership2' ); 
  244. break; 
  245.  
  246. case 'reversed': 
  247. $notes_pay = __( 'Last transaction has been reversed. Reason: Payment has been reversed (charge back).', 'membership2' ); 
  248. $status = MS_Model_Invoice::STATUS_DENIED; 
  249. $ignore = true; 
  250. break; 
  251.  
  252. case 'refunded': 
  253. $notes_pay = __( 'Last transaction has been reversed. Reason: Payment has been refunded.', 'membership2' ); 
  254. $status = MS_Model_Invoice::STATUS_DENIED; 
  255. $ignore = true; 
  256. break; 
  257.  
  258. case 'denied': 
  259. $notes_pay = __( 'Last transaction has been reversed. Reason: Payment Denied.', 'membership2' ); 
  260. $status = MS_Model_Invoice::STATUS_DENIED; 
  261. $ignore = true; 
  262. break; 
  263.  
  264. case 'pending': 
  265. lib3()->array->strip_slashes( $_POST, 'pending_reason' ); 
  266. $notes_pay = __( 'Last transaction is pending.', 'membership2' ) . ' '; 
  267.  
  268. switch ( $_POST['pending_reason'] ) { 
  269. case 'address': 
  270. $notes_pay .= __( 'Customer did not include a confirmed shipping address', 'membership2' ); 
  271. break; 
  272.  
  273. case 'authorization': 
  274. $notes_pay .= __( 'Funds not captured yet', 'membership2' ); 
  275. break; 
  276.  
  277. case 'echeck': 
  278. $notes_pay .= __( 'The eCheck has not cleared yet', 'membership2' ); 
  279. break; 
  280.  
  281. case 'intl': 
  282. $notes_pay .= __( 'Payment waiting for approval by service provider', 'membership2' ); 
  283. break; 
  284.  
  285. case 'multi-currency': 
  286. $notes_pay .= __( 'Payment waiting for service provider to handle multi-currency process', 'membership2' ); 
  287. break; 
  288.  
  289. case 'unilateral': 
  290. $notes_pay .= __( 'Customer did not register or confirm his/her email yet', 'membership2' ); 
  291. break; 
  292.  
  293. case 'upgrade': 
  294. $notes_pay .= __( 'Waiting for service provider to upgrade the PayPal account', 'membership2' ); 
  295. break; 
  296.  
  297. case 'verify': 
  298. $notes_pay .= __( 'Waiting for service provider to verify his/her PayPal account', 'membership2' ); 
  299. break; 
  300.  
  301. default: 
  302. $notes_pay .= __( 'Unknown reason', 'membership2' ); 
  303. break; 
  304.  
  305. $status = MS_Model_Invoice::STATUS_PENDING; 
  306. $ignore = true; 
  307. break; 
  308.  
  309. default: 
  310. case 'partially-refunded': 
  311. case 'in-progress': 
  312. $notes_pay = sprintf( 
  313. __( 'Not handling payment_status: %s', 'membership2' ),  
  314. $payment_status 
  315. ); 
  316. $ignore = true; 
  317. break; 
  318.  
  319. // Check for subscription details 
  320. if ( $transaction_type ) { 
  321. switch ( $transaction_type ) { 
  322. case 'subscr_signup': 
  323. case 'subscr_payment': 
  324. // Payment was received 
  325. $notes_txn = __( 'PayPal Subscripton has been created.', 'membership2' ); 
  326. if ( 0 == $invoice->total ) { 
  327. $success = true; 
  328. } else { 
  329. $ignore = true; 
  330. break; 
  331.  
  332. case 'subscr_modify': 
  333. // Payment profile was modified 
  334. $notes_txn = __( 'PayPal Subscription has been modified.', 'membership2' ); 
  335. $ignore = true; 
  336. break; 
  337.  
  338. case 'recurring_payment_profile_canceled': 
  339. case 'subscr_cancel': 
  340. // Subscription was manually cancelled. 
  341. $notes_txn = __( 'PayPal Subscription has been canceled.', 'membership2' ); 
  342. $member->cancel_membership( $membership->id ); 
  343. $member->save(); 
  344. $ignore = true; 
  345. break; 
  346.  
  347. case 'recurring_payment_suspended': 
  348. // Recurring subscription was manually suspended. 
  349. $notes_txn = __( 'PayPal Subscription has been suspended.', 'membership2' ); 
  350. $member->cancel_membership( $membership->id ); 
  351. $member->save(); 
  352. $ignore = true; 
  353. break; 
  354.  
  355. case 'recurring_payment_suspended_due_to_max_failed_payment': 
  356. // Recurring subscription was automatically suspended. 
  357. $notes_txn = __( 'PayPal Subscription has failed.', 'membership2' ); 
  358. $member->cancel_membership( $membership->id ); 
  359. $member->save(); 
  360. $ignore = true; 
  361. break; 
  362.  
  363. case 'new_case': 
  364. // New Dispute was filed for a payment. 
  365. $status = MS_Model_Invoice::STATUS_DENIED; 
  366. $ignore = true; 
  367. break; 
  368.  
  369. case 'subscr_eot': 
  370. /** 
  371. * Meaning: Subscription expired. 
  372. * - after a one-time payment was made 
  373. * - after last transaction in a recurring subscription 
  374. * - payment failed 
  375. * - ... 
  376. * We do not handle this event... 
  377. * One time payment sends 3 messages: 
  378. * 1. subscr_start (new subscription starts) 
  379. * 2. subscr_payment (payment confirmed) 
  380. * 3. subscr_eot (subscription ends) 
  381. */ 
  382. $notes_txn = __( 'No more payments will be made for this subscription.', 'membership2' ); 
  383. $ignore = true; 
  384. break; 
  385.  
  386. default: 
  387. // Other event that we do not have a case for... 
  388. $notes_txn = sprintf( 
  389. __( 'Not handling txn_type: %s', 'membership2' ),  
  390. $transaction_type 
  391. ); 
  392. $ignore = true; 
  393. break; 
  394.  
  395. if ( ! empty( $notes_pay ) ) { $invoice->add_notes( $notes_pay ); } 
  396. if ( ! empty( $notes_txn ) ) { $invoice->add_notes( $notes_txn ); } 
  397.  
  398. if ( $notes_pay ) { 
  399. $notes .= ($notes ? ' | ' : '') . $notes_pay; 
  400. if ( $notes_txn ) { 
  401. $notes .= ($notes ? ' | ' : '') . $notes_txn; 
  402.  
  403. $invoice->save(); 
  404.  
  405. if ( $success ) { 
  406. $invoice->pay_it( $this->id, $external_id ); 
  407. } elseif ( ! empty( $status ) ) { 
  408. $invoice->status = $status; 
  409. $invoice->save(); 
  410. $invoice->changed(); 
  411.  
  412. do_action( 
  413. 'ms_gateway_paypalstandard_payment_processed_' . $status,  
  414. $invoice,  
  415. $subscription 
  416. ); 
  417.  
  418. } else { 
  419. $reason = 'Unexpected transaction response'; 
  420. switch ( true ) { 
  421. case is_wp_error( $response ): 
  422. $reason = 'PayPal did not verify this transaction: Unknown error'; 
  423. break; 
  424.  
  425. case 200 != $response['response']['code']: 
  426. $reason = sprintf( 
  427. 'PayPal did not verify the transaction: Code %s',  
  428. $response['response']['code'] 
  429. ); 
  430. break; 
  431.  
  432. case empty( $response['body'] ): 
  433. $reason = 'PayPal did not verify this transaction: Empty response'; 
  434. break; 
  435.  
  436. case 'VERIFIED' != $response['body']: 
  437. $reason = sprintf( 
  438. 'PayPal did not verify this transaction: "%s"',  
  439. $response['body'] 
  440. ); 
  441. break; 
  442.  
  443. case ! $invoice->id: 
  444. $reason = sprintf( 
  445. 'Specified invoice does not exist: "%s"',  
  446. $invoice_id 
  447. ); 
  448. break; 
  449.  
  450. $notes = 'Response Error: ' . $reason; 
  451. $exit = true; 
  452. } else { 
  453. // Did not find expected POST variables. Possible access attempt from a non PayPal site. 
  454.  
  455. $u_agent = $_SERVER['HTTP_USER_AGENT']; 
  456. if ( ! $log && false === strpos( $u_agent, 'PayPal' ) ) { 
  457. // Very likely someone tried to open the URL manually. Redirect to home page 
  458. if ( ! $notes ) { 
  459. $notes = 'Ignored: Missing POST variables. Redirect to Home-URL.'; 
  460. $redirect = MS_Helper_Utility::home_url( '/' ); 
  461. $ignore = true; 
  462. $success = false; 
  463. } elseif ( 'm1' == $ext_type ) { 
  464. /** 
  465. * The payment belongs to an imported M1 subscription and could 
  466. * not be auto-matched. 
  467. * Do not return an error code, but also do not modify any 
  468. * invoice/subscription. 
  469. */ 
  470. $notes = 'M1 Payment detected. Manual matching required.'; 
  471. $ignore = false; 
  472. $success = false; 
  473. } elseif ( 'pay_btn' == $ext_type ) { 
  474. /** 
  475. * The payment was made by a PayPal Payment button that was 
  476. * created in the PayPal account and not by M1/M2. 
  477. */ 
  478. $notes = 'PayPal Payment button detected. Manual matching required.'; 
  479. $ignore = false; 
  480. $success = false; 
  481. } else { 
  482. // PayPal sent us a IPN notice about a non-Membership payment: 
  483. // Ignore it, but add it to the logs. 
  484.  
  485. if ( ! empty( $notes ) ) { 
  486. // We already have an error message, do nothing. 
  487. elseif ( ! $payment_status || ! $transaction_type ) { 
  488. $notes = 'Ignored: Payment_status or txn_type not specified. Cannot process.'; 
  489. } elseif ( empty( $_POST['invoice'] ) && empty( $_POST['custom'] ) ) { 
  490. $notes = 'Ignored: No invoice or custom data specified.'; 
  491. } else { 
  492. $notes = 'Ignored: Missing POST variables. Identification is not possible.'; 
  493.  
  494. $ignore = true; 
  495. $success = false; 
  496. $exit = true; 
  497.  
  498. if ( $ignore && ! $success ) { 
  499. $success = null; 
  500. $notes .= ' [Irrelevant IPN call]'; 
  501.  
  502. if ( ! $log ) { 
  503. do_action( 
  504. 'ms_gateway_transaction_log',  
  505. self::ID, // gateway ID 
  506. 'handle', // request|process|handle 
  507. $success, // success flag 
  508. $subscription_id, // subscription ID 
  509. $invoice_id, // invoice ID 
  510. $amount, // charged amount 
  511. $notes, // Descriptive text 
  512. $external_id // External ID 
  513. ); 
  514.  
  515. if ( $redirect ) { 
  516. wp_safe_redirect( $redirect ); 
  517. exit; 
  518. if ( $exit ) { 
  519. exit; 
  520. } else { 
  521. $log->invoice_id = $invoice_id; 
  522. $log->subscription_id = $subscription_id; 
  523. $log->amount = $amount; 
  524. $log->description = $notes; 
  525. $log->external_id = $external_id; 
  526. if ( $success ) { 
  527. $log->manual_state( 'ok' ); 
  528. } elseif ( $ignore ) { 
  529. $log->manual_state( 'ignore' ); 
  530. $log->save(); 
  531.  
  532. do_action( 
  533. 'ms_gateway_paypalstandard_handle_return_after',  
  534. $this 
  535. ); 
  536.  
  537. if ( $log ) { 
  538. return $log; 
  539.  
  540. /** 
  541. * Get paypal country sites list. 
  542. * @see MS_Gateway::get_country_codes() 
  543. * @since 1.0.0 
  544. * @return array 
  545. */ 
  546. public function get_paypal_sites() { 
  547. return apply_filters( 
  548. 'ms_gateway_paylpaystandard_get_paypal_sites',  
  549. self::get_country_codes() 
  550. ); 
  551.  
  552. /** 
  553. * Verify required fields. 
  554. * @since 1.0.0 
  555. * @return boolean 
  556. */ 
  557. public function is_configured() { 
  558. $is_configured = true; 
  559. $required = array( 'merchant_id', 'paypal_site' ); 
  560.  
  561. foreach ( $required as $field ) { 
  562. $value = $this->$field; 
  563. if ( empty( $value ) ) { 
  564. $is_configured = false; 
  565. break; 
  566.  
  567. return apply_filters( 
  568. 'ms_gateway_paypalstandard_is_configured',  
  569. $is_configured 
  570. ); 
  571.  
  572. /** 
  573. * Validate specific property before set. 
  574. * @since 1.0.0 
  575. * @access public 
  576. * @param string $name The name of a property to associate. 
  577. * @param mixed $value The value of a property. 
  578. */ 
  579. public function __set( $property, $value ) { 
  580. if ( property_exists( $this, $property ) ) { 
  581. switch ( $property ) { 
  582. case 'paypal_site': 
  583. if ( array_key_exists( $value, self::get_paypal_sites() ) ) { 
  584. $this->$property = $value; 
  585. break; 
  586.  
  587. default: 
  588. parent::__set( $property, $value ); 
  589. break; 
  590.  
  591. do_action( 
  592. 'ms_gateway_paypalstandard__set_after',  
  593. $property,  
  594. $value,  
  595. $this 
  596. ); 
  597.