Source: gateways/my-calendar-paypal.php

<?php
/**
 * My Calendar Submission PayPal Gateway
 *
 * @category Payments
 * @package  My Calendar Pro
 * @author   Joe Dolson
 * @license  GPLv2 or later
 * @link     https://www.joedolson.com/my-calendar-pro/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Insert PayPal form
 *
 * @param string $form Form HTML.
 * @param string $gateway Name of gateway.
 * @param array  $args Gateway arguments.
 *
 * @return string
 */
function mcs_paypal_form( $form, $gateway, $args ) {
	if ( 'paypal' === $gateway ) {
		$form .= mcs_paypal_insert_form( $args['price'], $args['currency'], $args['discount_rate'], $args['discounts'], $args['discount'] );
	}

	return $form;
}
add_filter( 'mcs_payment_gateway', 'mcs_paypal_form', 10, 3 );

/**
 * Provide response message from PayPal.
 *
 * @param array  $response Data from PayPal.
 * @param string $mcs Query string value.
 * @param array  $payment Payment data.
 * @param string $gateway Gateway name.
 *
 * @return string
 */
function mcs_paypal_response( $response, $mcs, $payment, $gateway ) {
	$ret = '';
	if ( 'paypal' === $gateway ) {
		$hash    = $payment['hash'];
		$receipt = add_query_arg( 'mcs_receipt', $hash, home_url() );
		switch ( $mcs ) {
			case 'thanks':
				// Translators: Receipt link.
				$ret = "<p class='notice'>" . sprintf( __( 'Thank you for your purchase! You can view your purchase information at PayPal. You will receive an email with your payment key once your payment is finalized. %s', 'my-calendar-pro' ), '<a href="' . $receipt . '">Receipt</a>' ) . '</p>';
				break;
			case 'cancel':
				$ret = __( 'Sorry that you decided to cancel your purchase! Contact us if you have any questions!', 'my-calendar-pro' );
				break;
		}
	}

	return $ret;
}
add_filter( 'mcs_gateway_response', 'mcs_paypal_response', 10, 4 );

/**
 * Process payment from PayPal.
 *
 * @param string $gateway Gateway.
 */
function mcs_paypal_processing( $gateway ) {
	if ( isset( $_POST['payment_status'] ) ) {
		$sandbox  = get_option( 'mcs_use_sandbox' );
		$receiver = strtolower( get_option( 'mcs_paypal_email' ) );
		$url      = ( 'true' === $sandbox ) ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
		$req      = 'cmd=_notify-validate';
		foreach ( $_POST as $key => $value ) {
			$value = urlencode( stripslashes( $value ) );
			$req  .= "&$key=$value";
		}
		$args   = wp_parse_args( $req, array() );
		$params = array(
			'body'        => $args,
			'timeout'     => 30,
			'user-agent'  => 'WordPress/My Calendar Pro',
			'httpversion' => '1.1',
		);

		// transaction variables to store.
		$payment_status   = sanitize_text_field( $_POST['payment_status'] );
		$item_number      = sanitize_text_field( $_POST['item_number'] );
		$price            = sanitize_text_field( $_POST['mc_gross'] );
		$payment_currency = sanitize_text_field( $_POST['mc_currency'] );
		$receiver_email   = sanitize_text_field( $_POST['receiver_email'] );
		$payer_email      = sanitize_text_field( $_POST['payer_email'] );
		$payer_first      = sanitize_text_field( $_POST['first_name'] );
		$payer_last       = sanitize_text_field( $_POST['last_name'] );
		$mc_fee           = sanitize_text_field( $_POST['mc_fee'] );
		$txn_id           = sanitize_text_field( $_POST['txn_id'] );
		// paypal IPN data.
		$ipn = wp_remote_post( $url, $params );

		if ( is_wp_error( $ipn ) ) {
			// send an email notification about this error.
			wp_mail( get_option( 'mcs_to' ), __( 'My Calendar Pro could not contact PayPal', 'my-calendar-pro' ), __( 'Here are the results of the query to PayPal:', 'my-calendar-pro' ) . "\n\n" . print_r( $ipn, 1 ) );
			status_header( 503 );
			die;
		}
		$response      = $ipn['body'];
		$response_code = $ipn['response']['code'];
		$currency      = get_option( 'mcs_currency' );
		// Placeholder; need to change the order creation process for this to work.
		$expected    = $price;
		$value_match = ( (float) $price === (float) $expected ) ? true : false;
		// die conditions for PayPal.
		// if receiver email or currency are wrong, this is probably a fraudulent transaction.
		if ( strtolower( $receiver_email ) !== $receiver || $payment_currency !== $currency ) {
			$error_msg = array();
			$messages  = '';
			if ( ! $value_match ) {
				// Translators: 1) Price passed, 2) Price expected.
				$error_msg[] = sprintf( __( 'Price paid did not match the price expected: %1$s vs %2$s', 'my-calendar-pro' ), $price, $expected );
			}
			if ( strtolower( $receiver_email ) !== $receiver ) {
				// Translators: 1) Email expected, 2) Email received.
				$error_msg[] = sprintf( __( 'Receiver Email and PayPal Email did not match: %1$s vs %2$s', 'my-calendar-pro' ), $receiver_email, $receiver );
			}
			if ( $payment_currency !== $currency ) {
				// Translators: 1) currency passed, 2) Currency expected.
				$error_msg[] = sprintf( __( 'Currency received did not match the currency expected: %1$s vs %2$s', 'my-calendar-pro' ), $payment_currency, $currency );
			}
			foreach ( $error_msg as $msg ) {
				$messages .= "\n\n" . $msg;
			}
			// Translators: Item Number of payment triggering error.
			wp_mail( get_option( 'mcs_to' ), __( 'Payment Conditions Error', 'my-calendar-pro' ), __( 'There were errors processing this payment:', 'my-calendar-pro' ) . $messages );
			// Send HTTP code 200 to stop PayPal from continuing to send this IPN query.
			status_header( 200 );
			die;
		}
	} else {
		wp_die( 'No valid IPN request made' );
	}
	if ( '200' === $response_code ) {
		if ( 'VERIFIED' === $response ) {
			$status = '';
			// See whether the transaction already exists. (For refunds, reversals, or canceled reversals).
			switch ( $payment_status ) {
				case 'Completed':
				case 'Created':
				case 'Denied':
				case 'Expired':
				case 'Failed':
				case 'Processed':
				case 'Voided':
					$status = $payment_status;
					break;
				case 'Pending':
					$status = $payment_status . ': ' . sanitize_text_field( $_POST['pending_reason'] );
					break;
				case 'Refunded':
				case 'Reversed':
				case 'Canceled_Reversal':
					$status = $payment_status . ': ' . sanitize_text_field( $_POST['ReasonCode'] );
					break;
			}
			$values = array(
				'item_number' => $item_number,
				'txn_id'      => $txn_id,
				'price'       => $price,
				'mc_fee'      => $mc_fee,
				'payer_first' => $payer_first,
				'payer_last'  => $payer_last,
				'payer_email' => $payer_email,
			);
			mcs_process_payment( $values, $status );
		} else {
			// log for manual investigation.
			$blog    = get_option( 'blogname' );
			$from    = "From: $blog Events <" . get_option( 'mcs_from' ) . '>';
			$subject = __( 'INVALID IPN on My Calendar Pro Payment', 'my-calendar-pro' );
			$body    = __( 'Something went wrong. Hopefully this information will help:', 'my-calendar-pro' ) . "\n\n";
			foreach ( $_POST as $key => $value ) {
				$body .= $key . ' = ' . $value . "\n";
			}
			wp_mail( get_option( 'mcs_to' ), $subject, $body, $from );
		}
	} else {
		$blog    = get_option( 'blogname' );
		$from    = "From: $blog Events <" . get_option( 'mcs_from' ) . '>';
		$subject = __( 'WordPress failed to contact PayPal', 'my-calendar-pro' );
		$body    = __( 'Something went wrong. Hopefully this information will help:', 'my-calendar-pro' ) . "\n\n";
		$body   .= print_r( $ipn, 1 );
		wp_mail( get_option( 'mcs_to' ), $subject, $body, $from );
	}

	status_header( 200 );
}
add_action( 'mcs_process_payment', 'mcs_paypal_processing' );

/**
 * Generate payment form
 *
 * @param float  $price Price.
 * @param string $currency Currency ID.
 * @param float  $discount_rate Discount percentage.
 * @param array  $discounts Discount info.
 * @param bool   $discount Is discount active.
 *
 * @return string
 */
function mcs_paypal_insert_form( $price, $currency, $discount_rate, $discounts, $discount ) {
	if ( ! get_option( 'mcs_paypal_merchant_id' ) && current_user_can( 'manage_options' ) ) {
		$form = '<p class="warning"><strong>' . __( 'PayPal account is not configured.', 'my-calendar-pro' ) . '</strong></p>';
	} else {
		$use_sandbox = get_option( 'mcs_use_sandbox' );
		$form        = "
		<form action='" . ( 'true' !== $use_sandbox ? 'https://www.paypal.com/cgi-bin/webscr' : 'https://www.sandbox.paypal.com/cgi-bin/webscr' ) . "' method='POST'>
			<input type='hidden' name='cmd' value='_xclick' />
			<input type='hidden' name='business' value='" . esc_attr( get_option( 'mcs_paypal_merchant_id' ) ) . "' />";
			// Translators: Site name.
		$form .= "<input type='hidden' name='item_name' value='" . esc_attr( sprintf( __( '%s Event Key Purchase', 'my-calendar-pro' ), get_option( 'blogname' ) ) ) . "' />
			<input type='hidden' name='item_number' value='1' />
			<input type='hidden' name='amount' value='" . esc_attr( $price ) . "' />
			<input type='hidden' name='no_shipping' value='1' />
			<input type='hidden' name='no_note' value='1' />
			<input type='hidden' name='currency_code' value='" . esc_attr( $currency ) . "' />";

		$form .= "
		<input type='hidden' name='notify_url' value='" . esc_url( add_query_arg( 'mcsipn', 'true', home_url() ) ) . "' />
		<input type='hidden' name='return' value='" . esc_url( add_query_arg( 'response_code', 'thanks', mc_get_current_url() ) ) . "' />
		<input type='hidden' name='cancel_return' value='" . esc_url( add_query_arg( 'response_code', 'cancel', mc_get_current_url() ) ) . "' />";
		if ( true === $discount && $discount_rate > 0 ) {
			$form .= "<input type='hidden' name='discount_rate' value='" . esc_attr( $discount_rate ) . "' />";
		}
		// API requires a button with the class 'mcs-change-quantity' to toggle in and out of payment form.
		// Translators: Amount owed.
		$form .= "<button type='submit' name='submit' class='button mcs-button'>" . sprintf( __( 'Pay - %s', 'my-calendar-pro' ), mcs_money_format( $price ) ) . "</button> <button type='button' class='mcs-change-quantity'>" . __( 'Change Quantity', 'my-calendar-pro' ) . '</button>';

		/**
		 * Insert additional HTML after PayPal payment button.
		 *
		 * @hook mcs_paypal_form
		 *
		 * @param {string} $form Additional HTMl inserted after inputs, inside form.
		 * @param {float}  $price Amount to submit to PayPal.
		 * @param {string} $currency Currency tag.
		 * @param {float}  $discount_rate Any applied discount rate.
		 * @param {array}  $discounts Information about available discounts.
		 * @param {bool}   $discount Whether discount is active.
		 *
		 * @return {string}
		 */
		$form .= apply_filters( 'mcs_paypal_form', '', $price, $currency, $discount_rate, $discounts, $discount );
		$form .= '</form>';
	}

	return $form;
}