File: /home/globfdxw/www/wp-content/plugins/wpforms-paypal-standard/src/Plugin.php
<?php
namespace WPFormsPaypalStandard;
use WP_Post;
use WPForms_Builder_Panel_Settings;
use WPForms_Payment;
use WPForms_Updater;
use WPFormsPaypalStandard\Migrations\Migrations;
use WPForms\Admin\Notice;
/**
* PayPal Standard integration.
*
* @since 1.0.0
*/
class Plugin extends WPForms_Payment {
/**
* Production mode.
*
* @since 1.5.0
*
* @var string
*/
const PRODUCTION = 'production';
/**
* Whether the payment has been allowed to process.
*
* @since 1.7.0
*
* @var bool
*/
private $allowed_to_process = false;
/**
* Form total amount.
*
* @since 1.7.0
*
* @var string
*/
private $amount = '';
/**
* Customer business email.
*
* @since 1.7.0
*
* @var string
*/
private $email = '';
/**
* Retrieve a single instance of the class.
*
* @since 1.8.0
*
* @return Plugin
*/
public static function get_instance() {
static $instance;
if ( ! $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Initialize.
*
* @since 1.0.0
*/
public function init() {
( new Migrations() )->init();
$this->version = WPFORMS_PAYPAL_STANDARD_VERSION;
$this->name = 'PayPal Standard';
$this->slug = 'paypal_standard';
$this->priority = 10;
$this->icon = WPFORMS_PAYPAL_STANDARD_URL . 'assets/images/addon-icon-paypal.png';
$this->hooks();
}
/**
* Add hooks.
*
* @since 1.8.0
*/
private function hooks() {
add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 );
add_action( 'wpforms_process_complete', [ $this, 'process_payment' ], 20, 4 );
add_filter( 'wpforms_forms_submission_prepare_payment_data', [ $this, 'prepare_payment_data' ], 10, 3 );
add_filter( 'wpforms_forms_submission_prepare_payment_meta', [ $this, 'prepare_payment_meta' ], 10, 3 );
add_action( 'wpforms_process_payment_saved', [ $this, 'process_payment_saved' ], 10, 3 );
add_action( 'init', [ $this, 'process_ipn' ] );
add_action( 'wpforms_form_settings_notifications_single_after', [ $this, 'notification_settings' ], 10, 2 );
add_filter( 'wpforms_entry_email_process', [ $this, 'process_email' ], 70, 5 );
add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] );
add_filter( 'wpforms_builder_strings', [ $this, 'javascript_strings' ], 10, 2 );
add_filter( 'wpforms_process_bypass_captcha', [ $this, 'bypass_captcha' ] );
add_action( 'admin_init', [ $this, 'promote_paypal_commerce_notice' ] );
add_action( 'wpforms_updater', [ $this, 'updater' ] );
}
/**
* Add admin notice to promote PayPal Standard addon.
*
* @since 1.10.0
*/
public function promote_paypal_commerce_notice() {
if ( ! wpforms_is_admin_page() ) {
return;
}
$notice = $this->get_promote_paypal_commerce_notice_msg();
Notice::info(
$notice,
[
'dismiss' => Notice::DISMISS_GLOBAL,
'slug' => 'paypal_commerce_promote',
'autop' => false,
]
);
}
/**
* Get notice message to promote PayPal Commerce addon.
*
* @since 1.10.0
*
* @return string
*/
private function get_promote_paypal_commerce_notice_msg() {
$is_builder_page = wpforms_is_admin_page( 'builder' );
$notice = sprintf(
wp_kses( /* translators: %s - PayPal Commerce addon page link. */
__(
'<p>The WPForms PayPal Standard integration has been deprecated.</p><p>PayPal Standard payments are not effected and will continue to process. However, the PayPal Standard addon will no longer receive updates.</p><p>We strongly recommend migrating to PayPal Commerce, which provides a seamless user experience and more features!</p>
<p><a href="%s" target="_blank" rel="noopener noreferrer">Learn More</a></p>',
'wpforms-paypal-standard'
),
[
'a' => [
'href' => true,
'rel' => true,
'target' => true,
],
'p' => [
'class' => true,
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/introducing-new-paypal-commerce-addon-for-wpforms/', $is_builder_page ? 'Builder Payments' : 'Settings', 'Upsell Paypal Commerce' ) )
);
if ( $is_builder_page ) {
$notice = sprintf( '<div class="wpforms-alert wpforms-alert-warning">%s</div>', $notice );
}
return $notice;
}
/**
* Load the plugin updater.
*
* @since 1.0.0
*
* @param string $key License key.
*/
public function updater( $key ) {
new WPForms_Updater(
[
'plugin_name' => 'WPForms PayPal Standard',
'plugin_slug' => 'wpforms-paypal-standard',
'plugin_path' => plugin_basename( WPFORMS_PAYPAL_STANDARD_FILE ),
'plugin_url' => trailingslashit( WPFORMS_PAYPAL_STANDARD_URL ),
'remote_url' => WPFORMS_UPDATER_API,
'version' => WPFORMS_PAYPAL_STANDARD_VERSION,
'key' => $key,
]
);
}
/**
* Display content inside the panel content area.
*
* @since 1.0.0
*/
public function builder_content() {
wpforms_panel_field(
'toggle',
$this->slug,
'enable',
$this->form_data,
esc_html__( 'Enable PayPal Standard payments', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'default' => '0',
]
);
echo '<div class="wpforms-panel-content-section-paypal-standard-body">';
echo $this->get_promote_paypal_commerce_notice_msg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->builder_email_field();
wpforms_panel_field(
'select',
$this->slug,
'mode',
$this->form_data,
esc_html__( 'Mode', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'default' => self::PRODUCTION,
'options' => [
'production' => esc_html__( 'Production', 'wpforms-paypal-standard' ),
'test' => esc_html__( 'Test / Sandbox', 'wpforms-paypal-standard' ),
],
'tooltip' => esc_html__( 'Select Production to receive real payments or select Test to use the PayPal developer sandbox', 'wpforms-paypal-standard' ),
]
);
wpforms_panel_field(
'select',
$this->slug,
'transaction',
$this->form_data,
esc_html__( 'Payment Type', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'default' => 'product',
'options' => [
'product' => esc_html__( 'Products and Services', 'wpforms-paypal-standard' ),
'donation' => esc_html__( 'Donation', 'wpforms-paypal-standard' ),
],
'tooltip' => esc_html__( 'Select the type of payment you are receiving.', 'wpforms-paypal-standard' ),
]
);
wpforms_panel_field(
'text',
$this->slug,
'cancel_url',
$this->form_data,
esc_html__( 'Cancel URL', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'tooltip' => esc_html__( 'Enter the URL to send users to if they do not complete the PayPal checkout', 'wpforms-paypal-standard' ),
]
);
wpforms_panel_field(
'select',
$this->slug,
'shipping',
$this->form_data,
esc_html__( 'Shipping', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'default' => '0',
'options' => [
'1' => esc_html__( 'Don\'t ask for an address', 'wpforms-paypal-standard' ),
'0' => esc_html__( 'Ask for an address, but do not require', 'wpforms-paypal-standard' ),
'2' => esc_html__( 'Ask for an address and require it', 'wpforms-paypal-standard' ),
],
]
);
wpforms_conditional_logic()->builder_block(
[
'form' => $this->form_data,
'type' => 'panel',
'panel' => $this->slug,
'parent' => 'payments',
'actions' => [
'go' => esc_html__( 'Process', 'wpforms-paypal-standard' ),
'stop' => esc_html__( 'Don\'t process', 'wpforms-paypal-standard' ),
],
'action_desc' => esc_html__( 'this charge if', 'wpforms-paypal-standard' ),
'reference' => esc_html__( 'PayPal Standard payment', 'wpforms-paypal-standard' ),
]
);
echo '</div>';
}
/**
* Add email field.
*
* @since 1.5.0
*/
private function builder_email_field() {
$is_production_mode = empty( $this->form_data['payments'][ $this->slug ] ) || $this->is_production_mode();
// Backward compatibility for forms created on addon's version < 1.5.0.
$fallback_email = ! empty( $this->form_data['payments'][ $this->slug ]['email'] ) ? $this->form_data['payments'][ $this->slug ]['email'] : '';
wpforms_panel_field(
'text',
$this->slug,
'production_email',
$this->form_data,
esc_html__( 'PayPal Email Address (Production)', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'placeholder' => esc_html__( 'Enter your business email address', 'wpforms-paypal-standard' ),
'class' => ! $is_production_mode ? 'wpforms-hidden' : '',
'default' => ! $is_production_mode ? '' : $fallback_email,
'after_tooltip' => ' <span class="required">*</span>',
'input_class' => 'wpforms-required',
]
);
wpforms_panel_field(
'text',
$this->slug,
'sandbox_email',
$this->form_data,
esc_html__( 'PayPal Email Address (Sandbox)', 'wpforms-paypal-standard' ),
[
'parent' => 'payments',
'placeholder' => esc_html__( 'Enter your business email address', 'wpforms-paypal-standard' ),
'class' => $is_production_mode ? 'wpforms-hidden' : '',
'default' => $is_production_mode ? '' : $fallback_email,
'after_tooltip' => ' <span class="required">*</span>',
'input_class' => 'wpforms-required',
]
);
}
/**
* Determine whether the payment can be processed.
*
* @since 1.0.0
*
* @param array $fields Final/sanitized submitted field data.
* @param array $entry Copy of the original $_POST.
* @param array $form_data Form data and settings.
*/
public function process_entry( $fields, $entry, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$this->form_data = $form_data;
$errors = [];
// Check if payment method exists.
if ( empty( $this->form_data['payments'][ $this->slug ] ) ) {
return;
}
// Check required payment settings.
$payment_settings = $this->form_data['payments'][ $this->slug ];
$this->email = $this->get_email();
if (
empty( $this->email ) ||
empty( $payment_settings['enable'] ) ||
(string) $payment_settings['enable'] !== '1'
) {
return;
}
if ( ! empty( wpforms()->get( 'process' )->errors[ $this->form_data['id'] ] ) ) {
return;
}
// If preventing the notification, log it, and then bail.
if ( ! $this->is_conditional_logic_ok( $fields ) ) {
$this->log_errors( esc_html__( 'PayPal Standard Payment stopped by conditional logic', 'wpforms-paypal-standard' ), $fields, 'conditional_logic' );
return;
}
// Check that, despite how the form is configured, the form and
// entry actually contain payment fields, otherwise no need to proceed.
$form_has_payments = wpforms_has_payment( 'form', $this->form_data );
$entry_has_paymemts = wpforms_has_payment( 'entry', $fields );
if ( ! $form_has_payments || ! $entry_has_paymemts ) {
$error_title = esc_html__( 'PayPal Standard Payment stopped, missing payment fields', 'wpforms-paypal-standard' );
$errors[] = $error_title;
$this->log_errors( $error_title );
} else {
// Check total charge amount.
$this->amount = wpforms_get_total_payment( $fields );
if ( empty( $this->amount ) || $this->amount === wpforms_sanitize_amount( 0 ) ) {
$error_title = esc_html__( 'PayPal Standard Payment stopped, invalid/empty amount', 'wpforms-paypal-standard' );
$errors[] = $error_title;
$this->log_errors( $error_title );
}
}
if ( $errors ) {
$this->display_errors( $errors );
return;
}
$this->allowed_to_process = true;
}
/**
* Update entry details and add meta for a successful payment.
*
* @since 1.7.0
*
* @param array $fields Final/sanitized submitted field data.
* @param array $entry Copy of the original $_POST.
* @param array $form_data Form data and settings.
* @param string $entry_id Entry ID.
*/
public function process_payment( $fields, $entry, $form_data, $entry_id ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded
// If payment has not been cleared for processing, return.
if ( empty( $entry_id ) || ! $this->allowed_to_process ) {
return;
}
// Update the entry type.
wpforms()->get( 'entry' )->update(
$entry_id,
[ 'type' => 'payment' ],
'',
'',
[ 'cap' => false ]
);
$payment_settings = $this->form_data['payments'][ $this->slug ];
// Build the return URL with hash.
$query_args = 'form_id=' . $this->form_data['id'] . '&entry_id=' . $entry_id . '&hash=' . wp_hash( $this->form_data['id'] . ',' . $entry_id );
$return_url = is_ssl() ? 'https://' : 'http://';
$server_name = isset( $_SERVER['SERVER_NAME'] ) ?
sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ) :
'';
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ?
esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) :
'';
$return_url .= $server_name . $request_uri;
if ( ! empty( $this->form_data['settings']['ajax_submit'] ) && ! empty( $_SERVER['HTTP_REFERER'] ) ) {
$return_url = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) );
}
$return_url = esc_url_raw(
add_query_arg(
[
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
'wpforms_return' => base64_encode( $query_args ),
],
/**
* PayPal Standard return URL.
*
* @since 1.0.0
*
* @param string $return_url Return URL.
* @param array $form_data Form data.
*/
apply_filters( 'wpforms_paypal_return_url', $return_url, $this->form_data ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
)
);
// Setup various vars.
$items = wpforms_get_payment_items( $fields );
$redirect = $this->is_production_mode() ? 'https://www.paypal.com/cgi-bin/webscr/?' : 'https://www.sandbox.paypal.com/cgi-bin/webscr/?';
$cancel_url = ! empty( $payment_settings['cancel_url'] ) ? esc_url_raw( $payment_settings['cancel_url'] ) : home_url();
$transaction = $payment_settings['transaction'] === 'donation' ? '_donations' : '_cart';
// Setup PayPal arguments.
$paypal_args = [
'bn' => 'WPForms_SP',
'lc' => get_user_locale(),
'business' => trim( $this->email ),
'cancel_return' => $cancel_url,
'cbt' => get_bloginfo( 'name' ),
'charset' => get_bloginfo( 'charset' ),
'cmd' => $transaction,
'currency_code' => strtoupper( wpforms_get_currency() ),
'custom' => absint( $this->form_data['id'] ),
'invoice' => $this->get_invoice_id( $entry_id ),
'no_shipping' => absint( $payment_settings['shipping'] ),
'notify_url' => add_query_arg( 'wpforms-listener', 'IPN', home_url( 'index.php' ) ),
'return' => $return_url,
'rm' => '1',
'tax' => 0,
'upload' => '1',
];
// Add cart items.
if ( $transaction === '_cart' ) {
// Product/service.
$i = 1;
foreach ( $items as $item ) {
if ( $this->is_empty_multiple_item( $item ) ) {
continue;
}
$item_amount = wpforms_sanitize_amount( $item['amount'] );
if ( empty( $item['name'] ) ) {
$item['name'] = sprintf(
'Field %s',
$item['id']
);
}
if (
! empty( $item['value_choice'] ) &&
in_array( $item['type'], [ 'payment-multiple', 'payment-select', 'payment-checkbox' ], true )
) {
$item['value_choice'] = ( $item['type'] === 'payment-checkbox' ) ? str_replace( "\r\n", ', ', $item['value_choice'] ) : $item['value_choice'];
$item_name = $item['name'] . ' - ' . $item['value_choice'];
} else {
$item_name = $item['name'];
}
$paypal_args[ 'item_name_' . $i ] = stripslashes_deep( html_entity_decode( $item_name, ENT_COMPAT, 'UTF-8' ) );
$paypal_args[ 'quantity_' . $i ] = $item['quantity'] ?? 1;
$paypal_args[ 'amount_' . $i ] = $item_amount;
$i ++;
}
} else {
// Combine a donation name from all payment fields names.
$item_names = [];
foreach ( $items as $item ) {
if ( $this->is_empty_multiple_item( $item ) ) {
continue;
}
if ( empty( $item['name'] ) ) {
$item['name'] = sprintf(
'Field %s',
$item['id']
);
}
if (
! empty( $item['value_choice'] ) &&
in_array( $item['type'], [ 'payment-multiple', 'payment-select', 'payment-checkbox' ], true )
) {
$item['value_choice'] = ( $item['type'] === 'payment-checkbox' ) ? str_replace( "\r\n", ', ', $item['value_choice'] ) : $item['value_choice'];
$item_name = $item['name'] . ' - ' . $item['value_choice'];
} else {
$item_name = $item['name'];
}
$item_names[] = stripslashes_deep( html_entity_decode( $item_name, ENT_COMPAT, 'UTF-8' ) );
}
$paypal_args['item_name'] = implode( '; ', $item_names );
$paypal_args['amount'] = $this->amount;
}
// Last change to filter args.
/**
* PayPal Standard redirect arguments.
*
* @since 1.0.0
*
* @param array $paypal_args PayPal arguments.
* @param array $fields Form fields.
* @param array $form_data Form data.
* @param string $entry_id Entry ID.
*/
$paypal_args = apply_filters( 'wpforms_paypal_redirect_args', $paypal_args, $fields, $this->form_data, $entry_id ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
// Build query.
$redirect .= http_build_query( $paypal_args );
$redirect = str_replace( '&', '&', $redirect );
// Redirect to PayPal.
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
wp_redirect( $redirect );
exit;
}
/**
* Add details to payment data.
*
* @since 1.7.0
*
* @param array $payment_data Payment data args.
* @param array $fields Form fields.
* @param array $form_data Form data.
*
* @return array
*/
public function prepare_payment_data( $payment_data, $fields, $form_data ) {
// If payment has not been cleared for processing, return.
if ( ! $this->allowed_to_process ) {
return $payment_data;
}
$payment_data['status'] = 'pending';
$payment_data['gateway'] = sanitize_key( $this->slug );
$payment_data['mode'] = $form_data['payments'][ $this->slug ]['mode'] === self::PRODUCTION ? 'live' : 'test';
return $payment_data;
}
/**
* Add payment meta for a successful one-time or subscription payment.
*
* @since 1.7.0
*
* @param array $payment_meta Payment meta.
* @param array $fields Sanitized submitted field data.
* @param array $form_data Form data and settings.
*
* @return array
*/
public function prepare_payment_meta( $payment_meta, $fields, $form_data ) {
// If payment has not been cleared for processing, return.
if ( ! $this->allowed_to_process ) {
return $payment_meta;
}
$payment_meta['method_type'] = 'PayPal';
return $payment_meta;
}
/**
* Add payment info for successful payment.
*
* @since 1.7.0
*
* @param int $payment_id Payment ID.
* @param array $fields Form fields.
* @param array $form_data Form data.
*/
public function process_payment_saved( $payment_id, $fields, $form_data ) {
$payment = wpforms()->get( 'payment' )->get( $payment_id );
// If payment is not found, bail.
if ( ! isset( $payment->id ) || ! $this->allowed_to_process ) {
return;
}
$this->add_payment_log(
$payment_id,
sprintf(
'PayPal Standard payment created. (Invoice ID: %s)',
$this->get_invoice_id( $payment->entry_id )
)
);
}
/**
* Add payment log record.
*
* @since 1.7.0
*
* @param int $payment_id Payment ID.
* @param string $value Log value.
*/
private function add_payment_log( $payment_id, $value ) {
wpforms()->get( 'payment_meta' )->add(
[
'payment_id' => $payment_id,
'meta_key' => 'log', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => wp_json_encode( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
[
'value' => $value,
'date' => gmdate( 'Y-m-d H:i:s' ),
]
),
]
);
}
/**
* Bypass captcha if payment has been processed.
*
* @since 1.8.0
*
* @param bool $bypass_captcha Whether to bypass captcha.
*
* @return bool
*/
public function bypass_captcha( $bypass_captcha ) {
if ( $bypass_captcha ) {
return $bypass_captcha;
}
return $this->allowed_to_process;
}
/**
* Determine if multiple payment element is empty e.g. checkboxes, multiple choices, dropdowns, etc.
*
* @since 1.7.0
*
* @param array $item Cart item.
*
* @return bool
*/
private function is_empty_multiple_item( $item ) {
if ( empty( $item['type'] ) ) {
return false;
}
if ( $item['type'] === 'payment-single' || ! in_array( $item['type'], wpforms_payment_fields(), true ) ) {
return false;
}
return empty( $item['value_raw'] );
}
/**
* Log payment error.
*
* @since 1.6.0
*
* @param string $title Error title.
* @param array|string $messages Error messages.
* @param string $level Error level to add to 'payment' error level.
*/
private function log_errors( $title, $messages = [], $level = 'error' ) {
wpforms_log(
$title,
$messages,
[
'type' => [ 'payment', $level ],
'form_id' => $this->form_data['id'],
]
);
}
/**
* Display form errors.
*
* @since 1.6.0
*
* @param array $errors Errors to display.
*/
private function display_errors( $errors ) {
if ( ! $errors || ! is_array( $errors ) ) {
return;
}
wpforms()->get( 'process' )->errors[ $this->form_data['id'] ]['footer'] = implode( '<br>', $errors );
}
/**
* Check if conditional logic check passes for the given settings.
*
* @since 1.4.0
*
* @param array $fields Form fields.
*
* @return bool
*/
private function is_conditional_logic_ok( $fields ) {
// Check for conditional logic.
if (
empty( $this->form_data['payments'][ $this->slug ]['conditional_logic'] ) &&
empty( $this->form_data['payments'][ $this->slug ]['conditional_type'] ) &&
empty( $this->form_data['payments'][ $this->slug ]['conditionals'] )
) {
return true;
}
// All conditional logic checks passed, continue with processing.
$process = wpforms_conditional_logic()->conditionals_process( $fields, $this->form_data, $this->form_data['payments'][ $this->slug ]['conditionals'] );
if ( $this->form_data['payments'][ $this->slug ]['conditional_type'] === 'stop' ) {
$process = ! $process;
}
return $process;
}
/**
* Process PayPal IPN.
*
* Adapted from EDD and the PHP PayPal IPN Class.
*
* @link https://github.com/easydigitaldownloads/Easy-Digital-Downloads/blob/master/includes/gateways/paypal-standard.php
* @link https://github.com/WadeShuler/PHP-PayPal-IPN/blob/master/src/IpnListener.php
*
* @since 1.0.0
*/
public function process_ipn() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded
// Verify the call back query and its method.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if (
! isset( $_GET['wpforms-listener'] ) ||
$_GET['wpforms-listener'] !== 'IPN' ||
(
isset( $_SERVER['REQUEST_METHOD'] ) &&
$_SERVER['REQUEST_METHOD'] !== 'POST'
)
) {
return;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// Set initial post data to empty string.
$post_data = '';
// Fallback just in case post_max_size is lower than needed.
if ( ini_get( 'allow_url_fopen' ) ) {
$post_data = file_get_contents( 'php://input' );
} else {
// If allow_url_fopen is not enabled, then make sure that post_max_size is large enough.
// phpcs:ignore WordPress.PHP.IniSet.Risky
ini_set( 'post_max_size', '12M' );
}
// Start the encoded data collection with notification command.
$encoded_data = 'cmd=_notify-validate';
// Verify there is a post_data.
if ( $post_data !== '' ) {
// Append the data.
$encoded_data .= ini_get( 'arg_separator.output' ) . $post_data;
} else {
// Check if POST is empty.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( empty( $_POST ) ) {
// Nothing to do.
return;
}
// Loop through each POST.
foreach ( $_POST as $key => $value ) {
// Encode the value and append the data.
$encoded_data .= ini_get( 'arg_separator.output' ) . "$key=" . rawurlencode( $value );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
// Convert collected post data to an array.
parse_str( $encoded_data, $data );
foreach ( $data as $key => $value ) {
if ( false !== strpos( $key, 'amp;' ) ) {
$new_key = str_replace( [ '&', 'amp;' ], '&', $key );
unset( $data[ $key ] );
$data[ $new_key ] = $value;
}
}
// Check if $post_data_array has been populated.
if ( ! is_array( $data ) || empty( $data ) || empty( $data['invoice'] ) ) {
return;
}
$error = '';
$invoice = explode( '-', $data['invoice'], 2 );
$entry_id = ! empty( $invoice[1] ) ? absint( $invoice[1] ) : absint( $data['invoice'] );
$payment = wpforms()->get( 'payment' )->get_by( 'entry_id', $entry_id );
$payment_status = strtolower( $data['payment_status'] );
$this->form_data = wpforms()->get( 'form' )->get(
$payment->form_id,
[
'content_only' => true,
]
);
// If payment or form doesn't exist, bail.
if ( empty( $payment ) || empty( $this->form_data ) ) {
return;
}
$is_production_mode = $payment->mode === 'live';
// Verify IPN with PayPal unless specifically disabled.
$remote_post_args = [
'method' => 'POST',
'timeout' => 45,
'redirection' => 5,
'httpversion' => '1.1',
'blocking' => true,
'headers' => [
'host' => $is_production_mode ? 'www.paypal.com' : 'www.sandbox.paypal.com',
'connection' => 'close',
'content-type' => 'application/x-www-form-urlencoded',
'post' => '/cgi-bin/webscr HTTP/1.1',
'user-agent' => 'WPForms IPN Verification',
],
'body' => $data,
];
$remote_post_url = $is_production_mode ? 'https://ipnpb.paypal.com/cgi-bin/webscr' : 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
$remote_post = wp_remote_post( $remote_post_url, $remote_post_args );
if ( is_wp_error( $remote_post ) || wp_remote_retrieve_body( $remote_post ) !== 'VERIFIED' ) {
wpforms_log(
'PayPal Standard IPN Error',
$remote_post,
[
'parent' => $entry_id,
'type' => [ 'error', 'payment' ],
'form_id' => $payment->form_id,
]
);
$this->add_payment_log( $payment->id, 'PayPal Standard IPN Error' );
return;
}
$is_refunded = $payment_status === 'refunded';
// Verify transaction type.
if ( ! $is_refunded && isset( $data['txn_type'] ) && $data['txn_type'] !== 'web_accept' && $data['txn_type'] !== 'cart' ) {
return;
}
$email = $this->get_email();
// Verify payment recipient emails match.
if ( empty( $email ) || strtolower( $data['business'] ) !== strtolower( trim( $email ) ) ) {
$error = 'Payment failed: recipient emails do not match';
// Verify payment currency.
} elseif ( empty( $payment->currency ) || strtolower( $data['mc_currency'] ) !== strtolower( $payment->currency ) ) {
$error = 'Payment failed: currency formats do not match';
// Verify payment amounts.
} elseif ( ! $is_refunded && ( empty( $payment->total_amount ) || number_format( (float) $data['mc_gross'] ) !== number_format( (float) $payment->total_amount ) ) ) {
$error = 'Payment failed: payment amounts do not match';
}
// If there was an error, log and update the payment status.
if ( ! empty( $error ) ) {
$this->add_payment_log( $payment->id, $error );
wpforms_log(
'PayPal Standard IPN Error',
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
sprintf( '%s - IPN data: %s', $error, '<pre>' . print_r( $data, true ) . '</pre>' ),
[
'parent' => $entry_id,
'type' => [ 'error', 'payment' ],
'form_id' => $payment->form_id,
]
);
$this->update_payment( $payment->id, [ 'status' => 'failed' ] );
return;
}
// Save the title column.
$this->update_payment( $payment->id, [ 'title' => $this->get_payment_title( $data ) ] );
// Get payment fields.
$payment_fields = (array) wpforms_get_payment_items( $this->form_data['fields'] );
// Completed payment.
if ( $payment_status === 'completed' ) {
$this->update_payment(
$payment->id,
[
'status' => 'completed',
'transaction_id' => sanitize_text_field( $data['txn_id'] ),
]
);
$this->add_payment_log(
$payment->id,
sprintf(
'PayPal Standard payment completed. (Transaction ID: %s)',
$data['txn_id']
)
);
$entry = wpforms()->get( 'entry' )->get( $entry_id );
if ( ! empty( $entry ) ) {
// Send notification emails if configured.
wpforms()->get( 'process' )->entry_email( wpforms_decode( $entry->fields ), [], $this->unset_non_paypal_notifications(), $entry_id, $this->slug );
}
} elseif ( $is_refunded ) {
$this->process_refund( $data, $payment );
} elseif ( $payment_status === 'pending' && isset( $data['pending_reason'] ) ) {
switch ( strtolower( $data['pending_reason'] ) ) {
case 'echeck':
$note = 'Payment made via eCheck and will clear automatically in 5-8 days';
break;
case 'address':
$note = 'Payment requires a confirmed customer address and must be accepted manually through PayPal';
break;
case 'intl':
$note = 'Payment must be accepted manually through PayPal due to international account regulations';
break;
case 'multi-currency':
$note = 'Payment received in non-shop currency and must be accepted manually through PayPal';
break;
case 'paymentreview':
case 'regulatory_review':
$note = 'Payment is being reviewed by PayPal staff as high-risk or in possible violation of government regulations';
break;
case 'unilateral':
$note = 'Payment was sent to non-confirmed or non-registered email address.';
break;
case 'upgrade':
$note = 'PayPal account must be upgraded before this payment can be accepted';
break;
case 'verify':
$note = 'PayPal account is not verified. Verify account in order to accept this payment';
break;
case 'other':
$note = 'Payment is pending for unknown reasons. Contact PayPal support for assistance';
break;
default:
$note = esc_html( $data['pending_reason'] );
break;
}
$this->add_payment_log( $payment->id, $note );
}
/**
* Completed PayPal Standard IPN call.
*
* @since 1.0.0
*
* @param array $fields Payment fields.
* @param array $form_data Form data.
* @param string $entry_id Entry ID.
* @param string $data Payment data.
*/
do_action( 'wpforms_paypal_standard_process_complete', $payment_fields, $this->form_data, $entry_id, $data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
exit;
}
/**
* Process refunded payment.
*
* @since 1.9.0
*
* @param array $data PayPal payment processed data.
* @param object $payment Payment object.
*
* @return void
*/
private function process_refund( $data, $payment ) {
$meta_handler = wpforms()->get( 'payment_meta' );
// No need to format amount since it already contains decimals, e.g. 5.25.
$last_refund = abs( (float) $data['mc_gross'] );
// Before comparing we need to sum up all refunds since this data is not available in PayPal data.
$total_refund = $last_refund + (float) $meta_handler->get_single( $payment->id, 'refunded_amount' );
$status = $total_refund < (float) $payment->total_amount ? 'partrefund' : 'refunded';
$this->update_payment( $payment->id, [ 'status' => $status ] );
$meta_handler->update_or_add(
$payment->id,
'refunded_amount',
$total_refund
);
$this->add_payment_log(
$payment->id,
sprintf( 'PayPal payment refunded. Refunded amount: %s', wpforms_format_amount( $last_refund, true, $data['mc_currency'] ) )
);
}
/**
* Unset non PayPal notifications before process.
*
* @since 1.4.0
*
* @return array
*/
private function unset_non_paypal_notifications() {
if ( empty( $this->form_data['settings']['notifications'] ) ) {
return $this->form_data;
}
foreach ( $this->form_data['settings']['notifications'] as $id => $notification ) {
if ( empty( $notification[ $this->slug ] ) ) {
unset( $this->form_data['settings']['notifications'][ $id ] );
}
}
return $this->form_data;
}
/**
* Add checkbox to form notification settings.
*
* @since 1.4.0
*
* @param WPForms_Builder_Panel_Settings $settings WPForms_Builder_Panel_Settings class instance.
* @param int $id Subsection ID.
*/
public function notification_settings( $settings, $id ) {
wpforms_panel_field(
'toggle',
'notifications',
$this->slug,
$settings->form_data,
esc_html__( 'Enable for PayPal Standard completed payments', 'wpforms-paypal-standard' ),
[
'parent' => 'settings',
'class' => empty( $settings->form_data['payments'][ $this->slug ]['enable'] ) ? 'wpforms-hidden' : '',
'input_class' => 'wpforms-radio-group wpforms-radio-group-' . $id . '-notification-by-status wpforms-radio-group-item-paypal_standard wpforms-notification-by-status-alert',
'subsection' => $id,
'tooltip' => wp_kses(
__( 'When enabled this notification will <em>only</em> be sent when a PayPal Standard payment has been successfully <strong>completed</strong>.', 'wpforms-paypal-standard' ),
[
'em' => [],
'strong' => [],
]
),
'data' => [
'radio-group' => $id . '-notification-by-status',
'provider-title' => esc_html__( 'PayPal Standard completed payments', 'wpforms-paypal-standard' ),
],
]
);
}
/**
* Logic that helps decide if we should send completed payments notifications.
*
* @since 1.4.0
*
* @param bool $process Whether to process or not.
* @param array $fields Form fields.
* @param array $form_data Form data.
* @param int $notification_id Notification ID.
* @param string $context The context of the current email process.
*
* @return bool
*/
public function process_email( $process, $fields, $form_data, $notification_id, $context ) {
if ( ! $process ) {
return false;
}
if ( empty( $form_data['payments'][ $this->slug ]['enable'] ) ) {
return $process;
}
if ( empty( $form_data['settings']['notifications'][ $notification_id ][ $this->slug ] ) ) {
return $process;
}
if ( ! $this->is_conditional_logic_ok( $fields ) ) {
return false;
}
return $context === $this->slug;
}
/**
* Enqueue assets for the builder.
*
* @since 1.5.0
*
* @param string $view Current view.
*/
public function enqueues( $view ) {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-builder-paypal-standard',
WPFORMS_PAYPAL_STANDARD_URL . "assets/js/admin-builder-paypal-standard{$min}.js",
[ 'wpforms-builder' ],
WPFORMS_PAYPAL_STANDARD_VERSION,
true
);
wp_localize_script(
'wpforms-builder-paypal-standard',
'wpforms_paypal_standard',
[
'payment_fields' => wpforms_payment_fields(),
]
);
}
/**
* Add localized strings to be available in the form builder.
*
* @since 1.5.0
*
* @param array $strings Form builder JS strings.
* @param WP_Post $form Current form.
*
* @return array
*/
public function javascript_strings( $strings, $form ) {
$strings['paypal_standard_required_email'] = sprintf(
wp_kses(
'<p>%s</p><p>%s</p>',
[
'p' => [],
'strong' => [],
]
),
__( 'PayPal Email Address must be filled in when using the PayPal payments.', 'wpforms-paypal-standard' ),
__( 'To proceed, please go to <strong>Payments ยป PayPal Standard</strong> and fill in <strong>PayPal Email Address</strong>.', 'wpforms-paypal-standard' )
);
$strings['paypal_standard_required_payment_field'] = sprintf(
wp_kses(
'<p>%s</p><p>%s</p>',
[
'p' => [],
'strong' => [],
]
),
__( 'PayPal Standard payments are enabled but not effective.', 'wpforms-paypal-standard' ),
__( 'At least one <strong>Payment Field</strong> should be added to the form.', 'wpforms-paypal-standard' )
);
return $strings;
}
/**
* Determine if production mode selected.
*
* @since 1.5.0
*
* @return bool
*/
private function is_production_mode() {
return isset( $this->form_data['payments'][ $this->slug ]['mode'] ) && $this->form_data['payments'][ $this->slug ]['mode'] === self::PRODUCTION;
}
/**
* Get email address value.
*
* @since 1.5.0
*
* @return string
*/
private function get_email() {
$settings = $this->form_data['payments'][ $this->slug ];
if ( isset( $settings['production_email'], $settings['sandbox_email'] ) ) {
return $this->is_production_mode() ? $settings['production_email'] : $settings['sandbox_email'];
}
// Backward compatibility for forms created on addon's version < 1.5.0.
return isset( $settings['email'] ) ? $settings['email'] : '';
}
/**
* Update payment data by ID.
*
* @since 1.7.0
*
* @param string|int $payment_id Payment ID.
* @param array $data Payment data.
*
* @return void
*/
private function update_payment( $payment_id, $data = [] ) {
if ( ! wpforms()->get( 'payment' )->update( $payment_id, $data, '', '', [ 'cap' => false ] ) ) {
wpforms_log(
'PayPal Standard IPN Error: Payment update failed',
[
'payment_id' => $payment_id,
'data' => $data,
]
);
exit;
}
}
/**
* Get Payment title.
*
* @since 1.7.0
*
* @param array $data Processed PayPal payment data.
*
* @return string
*/
private function get_payment_title( $data ) {
if ( ! empty( $data['first_name'] ) ) {
$last_name = isset( $data['last_name'] ) ? $data['last_name'] : '';
return trim( $data['first_name'] . ' ' . $last_name );
}
if ( ! empty( $data['payer_email'] ) ) {
return sanitize_email( $data['payer_email'] );
}
return '';
}
/**
* Get invoice ID.
*
* @since 1.7.0
*
* @param int $entry_id Entry ID.
*
* @return string
*/
private function get_invoice_id( $entry_id ) {
static $invoice_id;
if ( ! empty( $invoice_id ) ) {
return $invoice_id;
}
$invoice_id = sprintf( '%s-%d', hash( 'crc32b', home_url() ), absint( $entry_id ) );
return $invoice_id;
}
}