HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
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' => '&nbsp;<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' => '&nbsp;<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( '&amp;', '&', $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;', '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;
	}
}