swpm_handle_pp_ipn.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. require_once 'swpm_handle_subsc_ipn.php';
  3. class swpm_paypal_ipn_handler { // phpcs:ignore
  4. public $ipn_log = false; // bool: log IPN results to text file?
  5. public $ipn_log_file; // filename of the IPN log
  6. public $ipn_response; // holds the IPN response from paypal
  7. public $ipn_data = array(); // array contains the POST values for IPN
  8. public $fields = array(); // array holds the fields to submit to paypal
  9. public $sandbox_mode = false;
  10. public function __construct() {
  11. $this->paypal_url = 'https://www.paypal.com/cgi-bin/webscr';
  12. $this->ipn_log_file = 'ipn_handle_debug_swpm.log';
  13. $this->ipn_response = '';
  14. }
  15. public function swpm_validate_and_create_membership() {
  16. // Check Product Name , Price , Currency , Receivers email ,
  17. $error_msg = '';
  18. // Read the IPN and validate
  19. $gross_total = $this->ipn_data['mc_gross'];
  20. $transaction_type = $this->ipn_data['txn_type'];
  21. $txn_id = $this->ipn_data['txn_id'];
  22. $payment_status = $this->ipn_data['payment_status'];
  23. // Check payment status
  24. if ( ! empty( $payment_status ) ) {
  25. if ( 'Denied' == $payment_status ) {
  26. $this->debug_log( 'Payment status for this transaction is DENIED. You denied the transaction... most likely a cancellation of an eCheque. Nothing to do here.', false );
  27. return false;
  28. }
  29. if ( 'Canceled_Reversal' === $payment_status ) {
  30. $this->debug_log( 'This is a dispute closed notification in your favour. The plugin will not do anyting.', false );
  31. return true;
  32. }
  33. if ( 'Completed' !== $payment_status && 'Processed' !== $payment_status && 'Refunded' !== $payment_status && 'Reversed' !== $payment_status ) {
  34. $error_msg .= 'Funds have not been cleared yet. Transaction will be processed when the funds clear!';
  35. $this->debug_log( $error_msg, false );
  36. return false;
  37. }
  38. }
  39. // Check txn type
  40. if ( 'new_case' === $transaction_type ) {
  41. $this->debug_log( 'This is a dispute case. Nothing to do here.', true );
  42. return true;
  43. }
  44. $custom = urldecode( $this->ipn_data['custom'] );
  45. $this->ipn_data['custom'] = $custom;
  46. $customvariables = SwpmTransactions::parse_custom_var( $custom );
  47. // Handle refunds
  48. if ( $gross_total < 0 ) {
  49. // This is a refund or reversal
  50. $this->debug_log( 'This is a refund notification. Refund amount: ' . $gross_total, true );
  51. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data, true );
  52. return true;
  53. }
  54. if ( isset( $this->ipn_data['reason_code'] ) && 'refund' === $this->ipn_data['reason_code'] ) {
  55. $this->debug_log( 'This is a refund notification. Refund amount: ' . $gross_total, true );
  56. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data, true );
  57. return true;
  58. }
  59. if ( ( 'subscr_signup' === $transaction_type ) ) {
  60. $this->debug_log( 'Subscription signup IPN received... (handled by the subscription IPN handler)', true );
  61. // Code to handle the signup IPN for subscription
  62. $subsc_ref = $customvariables['subsc_ref'];
  63. if ( ! empty( $subsc_ref ) ) {
  64. $this->debug_log( 'Found a membership level ID. Creating member account...', true );
  65. $swpm_id = $customvariables['swpm_id'];
  66. swpm_handle_subsc_signup_stand_alone( $this->ipn_data, $subsc_ref, $this->ipn_data['subscr_id'], $swpm_id );
  67. // Handle customized subscription signup
  68. }
  69. return true;
  70. } elseif ( ( $transaction_type == 'subscr_cancel' ) || ( $transaction_type == 'subscr_eot' ) || ( $transaction_type == 'subscr_failed' ) ) {
  71. // Code to handle the IPN for subscription cancellation
  72. $this->debug_log( 'Subscription cancellation IPN received... (handled by the subscription IPN handler)', true );
  73. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data );
  74. return true;
  75. } else {
  76. $cart_items = array();
  77. $this->debug_log( 'Transaction Type: Buy Now/Subscribe', true );
  78. $item_number = $this->ipn_data['item_number'];
  79. $item_name = $this->ipn_data['item_name'];
  80. $quantity = $this->ipn_data['quantity'];
  81. $mc_gross = $this->ipn_data['mc_gross'];
  82. $mc_currency = $this->ipn_data['mc_currency'];
  83. $current_item = array(
  84. 'item_number' => $item_number,
  85. 'item_name' => $item_name,
  86. 'quantity' => $quantity,
  87. 'mc_gross' => $mc_gross,
  88. 'mc_currency' => $mc_currency,
  89. );
  90. array_push( $cart_items, $current_item );
  91. }
  92. /*** Duplicate IPN check ***/
  93. // Query the DB to check if we have already processed this transaction or not
  94. global $wpdb;
  95. $txn_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}swpm_payments_tbl WHERE txn_id = %s", $txn_id ), OBJECT );
  96. // And if we have already processed it, do nothing and return true
  97. if (!empty($txn_row)) {
  98. $this->debug_log( "This transaction has already been processed (".$txn_id."). Nothing to do here.", true );
  99. return true;
  100. }
  101. /*** End of duplicate IPN check ***/
  102. $counter = 0;
  103. foreach ( $cart_items as $current_cart_item ) {
  104. $cart_item_data_num = $current_cart_item['item_number'];
  105. $cart_item_data_name = trim( $current_cart_item['item_name'] );
  106. $cart_item_data_quantity = $current_cart_item['quantity'];
  107. $cart_item_data_total = $current_cart_item['mc_gross'];
  108. $cart_item_data_currency = $current_cart_item['mc_currency'];
  109. if ( empty( $cart_item_data_quantity ) ) {
  110. $cart_item_data_quantity = 1;
  111. }
  112. $this->debug_log( 'Item Number: ' . $cart_item_data_num, true );
  113. $this->debug_log( 'Item Name: ' . $cart_item_data_name, true );
  114. $this->debug_log( 'Item Quantity: ' . $cart_item_data_quantity, true );
  115. $this->debug_log( 'Item Total: ' . $cart_item_data_total, true );
  116. $this->debug_log( 'Item Currency: ' . $cart_item_data_currency, true );
  117. // Get the button id
  118. $pp_hosted_button = false;
  119. $button_id = $cart_item_data_num;// Button id is the item number.
  120. $membership_level_id = get_post_meta( $button_id, 'membership_level_id', true );
  121. if ( ! SwpmUtils::membership_level_id_exists( $membership_level_id ) ) {
  122. $this->debug_log( 'This payment button was not created in the plugin. This is a paypal hosted button.', true );
  123. $pp_hosted_button = true;
  124. }
  125. // Price check
  126. $check_price = true;
  127. $msg = '';
  128. $msg = apply_filters( 'swpm_before_price_check_filter', $msg, $current_cart_item );
  129. if ( ! empty( $msg ) && $msg == 'price-check-override' ) {// This filter allows an extension to do a customized version of price check (if needed)
  130. $check_price = false;
  131. $this->debug_log( 'Price and currency check has been overridden by an addon/extension.', true );
  132. }
  133. if ( $check_price && ! $pp_hosted_button ) {
  134. // Check according to buy now payment or subscription payment.
  135. $button_type = get_post_meta( $button_id, 'button_type', true );
  136. if ( $button_type == 'pp_buy_now' ) {// This is a PayPal buy now type button
  137. $expected_amount = ( get_post_meta( $button_id, 'payment_amount', true ) ) * $cart_item_data_quantity;
  138. $expected_amount = round( $expected_amount, 2 );
  139. $expected_amount = apply_filters( 'swpm_payment_amount_filter', $expected_amount, $button_id );
  140. $received_amount = $cart_item_data_total;
  141. if ( $received_amount < $expected_amount ) {
  142. // Error! amount received is less than expected. This is invalid.
  143. $this->debug_log( 'Expected amount: ' . $expected_amount, true );
  144. $this->debug_log( 'Received amount: ' . $received_amount, true );
  145. $this->debug_log( 'Price check failed. Amount received is less than the amount expected. This payment will not be processed.', false );
  146. return false;
  147. }
  148. } elseif ( $button_type == 'pp_subscription' ) {// This is a PayPal subscription type button
  149. //This is a "subscr_payment" type payment notification. The "subscr_signup" type gets handled before.
  150. $trial_billing_cycle = get_post_meta( $button_id, 'trial_billing_cycle', true );
  151. $trial_billing_amount = get_post_meta( $button_id, 'trial_billing_amount', true );
  152. $billing_amount = get_post_meta( $button_id, 'billing_amount', true );
  153. if ( empty( $trial_billing_cycle ) ){
  154. //No trial billing. Check main billing amount. Only need to check "mc_gross" which should cointain the "amount3" value.
  155. $this->debug_log( 'Trial billing is not enabled for this button.', true );
  156. $expected_amount = round( $billing_amount, 2 );
  157. } else {
  158. //Trial billing is specified for this button
  159. $this->debug_log( 'Trial billing is enabled for this button.', true );
  160. if ( swpm_is_paypal_recurring_payment($this->ipn_data) ){
  161. //This is a recurring payment of a subscription.
  162. $expected_amount = round( $billing_amount, 2 );
  163. } else {
  164. //This is a trial payment of a subscription
  165. $expected_amount = round( $trial_billing_amount, 2 );
  166. }
  167. }
  168. $received_amount = $cart_item_data_total;
  169. if ( $received_amount < $expected_amount ) {
  170. // Error! amount received is less than expected. This is invalid.
  171. $this->debug_log( 'Expected amount: ' . $expected_amount, true );
  172. $this->debug_log( 'Received amount: ' . $received_amount, true );
  173. $this->debug_log( 'Price check failed. Amount received is less than the amount expected. This payment will not be processed.', false );
  174. return false;
  175. }
  176. } else {
  177. $this->debug_log( 'Error! Unexpected button type: ' . $button_type, false );
  178. return false;
  179. }
  180. }
  181. // *** Handle Membership Payment ***
  182. // --------------------------------------------------------------------------------------
  183. // ========= Need to find the (level ID) in the custom variable ============
  184. $subsc_ref = $customvariables['subsc_ref'];// Membership level ID
  185. $this->debug_log( 'Membership payment paid for membership level ID: ' . $subsc_ref, true );
  186. if ( ! empty( $subsc_ref ) ) {
  187. $swpm_id = '';
  188. if ( isset( $customvariables['swpm_id'] ) ) {
  189. $swpm_id = $customvariables['swpm_id'];
  190. }
  191. if ( $transaction_type == 'web_accept' ) {
  192. $this->debug_log( 'Transaction type: web_accept. Creating member account...', true );
  193. swpm_handle_subsc_signup_stand_alone( $this->ipn_data, $subsc_ref, $this->ipn_data['txn_id'], $swpm_id );
  194. } elseif ( $transaction_type == 'subscr_payment' ) {
  195. $this->debug_log( 'Transaction type: subscr_payment. Checking if the member profile needed to be updated', true );
  196. swpm_update_member_subscription_start_date_if_applicable( $this->ipn_data );
  197. }
  198. } else {
  199. $this->debug_log( 'Membership level ID is missing in the payment notification! Cannot process this notification.', false );
  200. }
  201. // == End of Membership payment handling ==
  202. $counter++;
  203. }
  204. /*** Do Post payment operation and cleanup */
  205. // Save the transaction data
  206. $this->debug_log( 'Saving transaction data to the database table.', true );
  207. $this->ipn_data['gateway'] = 'paypal';
  208. $this->ipn_data['status'] = $this->ipn_data['payment_status'];
  209. // If the value ipn_data['ip'] is empty, try to detect the customer IP address using the variable custom['user_ip']
  210. if (empty($this->ipn_data['ip']) && filter_var($customvariables['user_ip'], FILTER_VALIDATE_IP)) {
  211. $this->ipn_data['ip'] = $customvariables['user_ip'];
  212. }
  213. SwpmTransactions::save_txn_record( $this->ipn_data, $cart_items );
  214. $this->debug_log( 'Transaction data saved.', true );
  215. // Trigger the PayPal IPN processed action hook (so other plugins can can listen for this event).
  216. do_action( 'swpm_paypal_ipn_processed', $this->ipn_data );
  217. do_action( 'swpm_payment_ipn_processed', $this->ipn_data );
  218. return true;
  219. }
  220. public function swpm_validate_ipn() {
  221. // Generate the post string from the _POST vars aswell as load the _POST vars into an arry
  222. $post_string = '';
  223. foreach ( $_POST as $field => $value ) {
  224. $this->ipn_data[ "$field" ] = $value;
  225. $post_string .= $field . '=' . urlencode( stripslashes( $value ) ) . '&';
  226. }
  227. $this->post_string = $post_string;
  228. $this->debug_log( 'Post string : ' . $this->post_string, true );
  229. // IPN validation check
  230. if ( $this->validate_ipn_using_remote_post() ) {
  231. // We can also use an alternative validation using the validate_ipn_using_curl() function
  232. return true;
  233. } else {
  234. return false;
  235. }
  236. }
  237. public function validate_ipn_using_remote_post() {
  238. $this->debug_log( 'Checking if PayPal IPN response is valid', true );
  239. // Get received values from post data
  240. $validate_ipn = array( 'cmd' => '_notify-validate' );
  241. $validate_ipn += wp_unslash( $_POST );
  242. // Send back post vars to paypal
  243. $params = array(
  244. 'body' => $validate_ipn,
  245. 'timeout' => 60,
  246. 'httpversion' => '1.1',
  247. 'compress' => false,
  248. 'decompress' => false,
  249. 'user-agent' => 'Simple Membership Plugin',
  250. );
  251. // Post back to get a response.
  252. $connection_url = $this->sandbox_mode ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
  253. $this->debug_log( 'Connecting to: ' . $connection_url, true );
  254. $response = wp_safe_remote_post( $connection_url, $params );
  255. // The following two lines can be used for debugging
  256. // $this->debug_log( 'IPN Request: ' . print_r( $params, true ) , true);
  257. // $this->debug_log( 'IPN Response: ' . print_r( $response, true ), true);
  258. // Check to see if the request was valid.
  259. if ( ! is_wp_error( $response ) && strstr( $response['body'], 'VERIFIED' ) ) {
  260. $this->debug_log( 'IPN successfully verified.', true );
  261. return true;
  262. }
  263. // Invalid IPN transaction. Check the log for details.
  264. $this->debug_log( 'IPN validation failed.', false );
  265. if ( is_wp_error( $response ) ) {
  266. $this->debug_log( 'Error response: ' . $response->get_error_message(), false );
  267. }
  268. return false;
  269. }
  270. public function debug_log( $message, $success, $end = false ) {
  271. SwpmLog::log_simple_debug( $message, $success, $end );
  272. }
  273. }
  274. // Start of IPN handling (script execution)
  275. $ipn_handler_instance = new swpm_paypal_ipn_handler();
  276. $settings = SwpmSettings::get_instance();
  277. $debug_enabled = $settings->get_value( 'enable-debug' );
  278. if ( ! empty( $debug_enabled ) ) {
  279. $debug_log = 'log.txt'; // Debug log file name
  280. echo esc_html( sprintf( 'Debug logging is enabled. Check the %s file for debug output.', $debug_log ) );
  281. $ipn_handler_instance->ipn_log = true;
  282. $ipn_handler_instance->ipn_log_file = $debug_log;
  283. if ( empty( $_POST ) ) {
  284. $ipn_handler_instance->debug_log( 'This debug line was generated because you entered the URL of the ipn handling script in the browser.', true, true );
  285. exit;
  286. }
  287. }
  288. $sandbox_enabled = $settings->get_value( 'enable-sandbox-testing' );
  289. if ( ! empty( $sandbox_enabled ) ) {
  290. $ipn_handler_instance->paypal_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  291. $ipn_handler_instance->sandbox_mode = true;
  292. }
  293. $ipn_handler_instance->debug_log( 'Paypal Class Initiated by ' . $_SERVER['REMOTE_ADDR'], true );
  294. // Validate the IPN
  295. if ( $ipn_handler_instance->swpm_validate_ipn() ) {
  296. $ipn_handler_instance->debug_log( 'Creating product Information to send.', true );
  297. if ( ! $ipn_handler_instance->swpm_validate_and_create_membership() ) {
  298. $ipn_handler_instance->debug_log( 'IPN product validation failed.', false );
  299. }
  300. }
  301. $ipn_handler_instance->debug_log( 'Paypal class finished.', true, true );