/app/gateway/paypalstandard/class-ms-gateway-paypalstandard.php

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