laipower/wp-content/plugins/easy-digital-downloads/includes/gateways/stripe/includes/functions.php

775 lines
21 KiB
PHP

<?php
/**
* Functions
*
* @package EDD_Stripe
* @since unknown
*/
use EDD\Orders\Order;
/**
* Returns the one true instance of EDD_Stripe
*
* @since 2.6
*
* @return void|\EDD_Stripe EDD_Stripe instance or void if Easy Digital
* Downloads is not active.
*/
function edd_stripe() {
// Easy Digital Downloads is not active, do nothing.
if ( ! function_exists( 'EDD' ) ) {
return;
}
return EDD_Stripe::instance();
}
/**
* Determines if the current execution of Stripe is being loaded from the
* "Pro" version of the gateway.
*
* (Currently) when Stripe is packaged with EDD core the bootstrap file is
* renamed to `edds_stripe_bootstrap_core()` to prevent collision.
*
* @since 2.8.1
*
* @return bool True if the "Pro" version of the gateway is loaded.
*/
function edds_is_pro() {
return function_exists( 'edd_stripe_bootstrap' );
}
/**
* Determines if the Stripe gateway is active.
*
* This checks both application requirements being met as
* well as the gateway being enabled via Payment Gateways settings.
*
* @since 2.8.1
*
* @return bool
*/
function edds_is_gateway_active() {
if ( false === edds_has_met_requirements() ) {
return false;
}
if ( false === edd_is_gateway_active( 'stripe' ) ) {
return false;
}
return true;
}
/**
* Determines of the application requirements have been met.
*
* @since 2.8.1
*
* @param false|string $requirement Specific requirement to check for.
* False ensures all requirements are met.
* Default false.
* @return bool True if the requirement(s) is met.
*/
function edds_has_met_requirements( $requirement = false ) {
$requirements = array(
'php' => (
version_compare( PHP_VERSION, '5.6.0', '>' )
),
'edd' => (
defined( 'EDD_VERSION' )
? version_compare( EDD_VERSION, '2.11', '>=' )
: true
),
'recurring' => (
defined( 'EDD_RECURRING_VERSION' )
? version_compare( EDD_RECURRING_VERSION, '2.9.99', '>' )
: true
),
'wp' => (
version_compare( get_bloginfo( 'version' ), '4.9', '>=' )
),
);
if ( false === $requirement ) {
return false === in_array( false, $requirements, true );
} else {
return isset( $requirements[ $requirement ] )
? $requirements[ $requirement ]
: true;
}
}
/**
* Allows preconfigured Stripe API requests to be made.
*
* @since 2.7.0
*
* @throws \EDD_Stripe_Utils_Exceptions_Stripe_Object_Not_Found When attempting to call an object or method that is not available.
* @throws \Stripe\Exception
*
* @param string $object Name of the Stripe object to request.
* @param string $method Name of the API operation to perform during the request.
* @param mixed ...$args Additional arguments to pass to the request.
* @return \Stripe\StripeObject
*/
function edds_api_request( $object, $method, $args = null ) {
$api = new EDD_Stripe_API();
return call_user_func_array( array( $api, 'request' ), func_get_args() );
}
/**
* Converts a truthy value (e.g. '1', 'yes') to a bool.
*
* @since 2.8.0
*
* @param mixed $truthy_value Truthy value.
* @return bool
*/
function edds_truthy_to_bool( $truthy_value ) {
$truthy = array(
'yes',
1,
'1',
'true',
);
return is_bool( $truthy_value )
? $truthy_value
: in_array( strtolower( $truthy_value ), $truthy, true );
}
/**
* Retrieve the exsting cards setting.
* @return bool
*/
function edd_stripe_existing_cards_enabled() {
$use_existing_cards = edd_get_option( 'stripe_use_existing_cards', false );
return ! empty( $use_existing_cards );
}
/**
* Given a user ID, retrieve existing cards within stripe.
*
* @since 2.6
* @param int $user_id
*
* @return array
*/
function edd_stripe_get_existing_cards( $user_id = 0 ) {
if ( empty( $user_id ) ) {
return array();
}
$enabled = edd_stripe_existing_cards_enabled();
if ( ! $enabled ) {
return array();
}
static $existing_cards;
if ( ! is_null( $existing_cards ) && array_key_exists( $user_id, $existing_cards ) ) {
return $existing_cards[ $user_id ];
}
// Check if the user has existing cards
$customer_cards = array();
$stripe_customer_id = edds_get_stripe_customer_id( $user_id );
if ( ! empty( $stripe_customer_id ) ) {
try {
$stripe_customer = edds_api_request( 'Customer', 'retrieve', $stripe_customer_id );
if ( isset( $stripe_customer->deleted ) && $stripe_customer->deleted ) {
return $customer_cards;
}
$payment_methods = edds_api_request( 'PaymentMethod', 'all', array(
'type' => 'card',
'customer' => $stripe_customer->id,
'limit' => 100,
) );
$cards = edds_api_request( 'Customer', 'allSources', $stripe_customer->id, array(
'limit' => 100,
) );
$sources = array_merge( $payment_methods->data, $cards->data );
foreach ( $sources as $source ) {
if ( ! in_array( $source->object, array( 'payment_method', 'card' ), true ) ) {
continue;
}
$source_data = new stdClass();
$source_data->id = $source->id;
switch( $source->object ) {
case 'payment_method':
$source_data->brand = ucwords( $source->card->brand );
$source_data->last4 = $source->card->last4;
$source_data->exp_month = $source->card->exp_month;
$source_data->exp_year = $source->card->exp_year;
$source_data->fingerprint = $source->card->fingerprint;
$source_data->address_line1 = $source->billing_details->address->line1;
$source_data->address_line2 = $source->billing_details->address->line2;
$source_data->address_city = $source->billing_details->address->city;
$source_data->address_zip = $source->billing_details->address->postal_code;
$source_data->address_state = $source->billing_details->address->state;
$source_data->address_country = $source->billing_details->address->country;
$customer_cards[ $source->id ]['default'] = $source->id === $stripe_customer->invoice_settings->default_payment_method;
break;
case 'card':
$source_data->brand = $source->brand;
$source_data->last4 = $source->last4;
$source_data->exp_month = $source->exp_month;
$source_data->exp_year = $source->exp_year;
$source_data->fingerprint = $source->fingerprint;
$source_data->address_line1 = $source->address_line1;
$source_data->address_line2 = $source->address_line2;
$source_data->address_city = $source->address_city;
$source_data->address_zip = $source->address_zip;
$source_data->address_state = $source->address_state;
$source_data->address_country = $source->address_country;
break;
}
$customer_cards[ $source->id ]['source'] = $source_data;
}
} catch ( Exception $e ) {
return $customer_cards;
}
}
// Show only the latest version of card for duplicates.
$fingerprints = array();
foreach ( $customer_cards as $key => $customer_card ) {
$fingerprint = $customer_card['source']->fingerprint;
if ( ! in_array( $fingerprint, $fingerprints ) ) {
$fingerprints[] = $fingerprint;
} else {
unset( $customer_cards[ $key ] );
}
}
// Put default card first.
usort(
$customer_cards,
function( $a, $b ) {
return $a['default'] ? 1 : -1;
}
);
$existing_cards[ $user_id ] = $customer_cards;
return $existing_cards[ $user_id ];
}
/**
* Look up the stripe customer id in user meta, and look to recurring if not found yet
*
* @since 2.4.4
* @since 2.6 Added lazy load for moving to customer meta and ability to look up by customer ID.
* @param int $id_or_email The user ID, customer ID or email to look up.
* @param bool $by_user_id If the lookup is by user ID or not.
*
* @return string Stripe customer ID
*/
function edds_get_stripe_customer_id( $id_or_email, $by_user_id = true ) {
$stripe_customer_id = '';
$meta_key = edd_stripe_get_customer_key();
if ( is_email( $id_or_email ) ) {
$by_user_id = false;
}
$customer = new EDD_Customer( $id_or_email, $by_user_id );
if ( $customer->id > 0 ) {
$stripe_customer_id = $customer->get_meta( $meta_key );
}
if ( empty( $stripe_customer_id ) ) {
$user_id = 0;
if ( ! empty( $customer->user_id ) ) {
$user_id = $customer->user_id;
} else if ( $by_user_id && is_numeric( $id_or_email ) ) {
$user_id = $id_or_email;
} else if ( is_email( $id_or_email ) ) {
$user = get_user_by( 'email', $id_or_email );
if ( $user ) {
$user_id = $user->ID;
}
}
if ( ! isset( $user ) ) {
$user = get_user_by( 'id', $user_id );
}
if ( $user ) {
$customer = new EDD_Customer( $user->user_email );
if ( ! empty( $user_id ) ) {
$stripe_customer_id = get_user_meta( $user_id, $meta_key, true );
// Lazy load migrating data over to the customer meta from Stripe issue #113
$customer->update_meta( $meta_key, $stripe_customer_id );
}
}
}
if ( empty( $stripe_customer_id ) && class_exists( 'EDD_Recurring_Subscriber' ) ) {
$subscriber = new EDD_Recurring_Subscriber( $id_or_email, $by_user_id );
if ( $subscriber->id > 0 ) {
$verified = false;
if ( ( $by_user_id && $id_or_email == $subscriber->user_id ) ) {
// If the user ID given, matches that of the subscriber
$verified = true;
} else {
// If the email used is the same as the primary email
if ( $subscriber->email == $id_or_email ) {
$verified = true;
}
// If the email is in the EDD 2.6 Additional Emails
if ( property_exists( $subscriber, 'emails' ) && in_array( $id_or_email, $subscriber->emails ) ) {
$verified = true;
}
}
if ( $verified ) {
$stripe_customer_id = $subscriber->get_recurring_customer_id( 'stripe' );
}
}
if ( ! empty( $stripe_customer_id ) ) {
$customer->update_meta( $meta_key, $stripe_customer_id );
}
}
return $stripe_customer_id;
}
/**
* Get the meta key for storing Stripe customer IDs in
*
* @access public
* @since 1.6.7
* @return string
*/
function edd_stripe_get_customer_key() {
$key = '_edd_stripe_customer_id';
if( edd_is_test_mode() ) {
$key .= '_test';
}
return $key;
}
/**
* Determines if the provided currency is a zero-decimal currency
*
* @access public
* @since 1.8.4
* @param string $currency Three-letter ISO currency code or an empty string. If empty, the shop's currency is used.
* @since 2.8.8 $currency parameter added
* @return bool
*/
function edds_is_zero_decimal_currency( $currency = '' ) {
if ( empty( $currency ) ) {
$currency = edd_get_currency();
}
$currency = strtolower( $currency );
$currencies = array(
'bif',
'clp',
'djf',
'gnf',
'jpy',
'kmf',
'krw',
'mga',
'pyg',
'rwf',
'ugx',
'vnd',
'vuv',
'xaf',
'xof',
'xpf',
);
return in_array( $currency, $currencies, true );
}
/**
* Retrieves a sanitized statement descriptor.
*
* @since 2.6.19
*
* @return string $statement_descriptor Sanitized statement descriptor.
*/
function edds_get_statement_descriptor() {
$statement_descriptor = edd_get_option( 'stripe_statement_descriptor', '' );
$statement_descriptor = edds_sanitize_statement_descriptor( $statement_descriptor );
return $statement_descriptor;
}
/**
* Retrieves a list of unsupported characters for Stripe statement descriptors.
*
* @since 2.6.19
*
* @return array $unsupported_characters List of unsupported characters.
*/
function edds_get_statement_descriptor_unsupported_characters() {
$unsupported_characters = array(
'<',
'>',
'"',
'\'',
'\\',
'*',
);
/**
* Filters the list of unsupported characters for Stripe statement descriptors.
*
* @since 2.6.19
*
* @param array $unsupported_characters List of unsupported characters.
*/
$unsupported_characters = apply_filters( 'edds_get_statement_descriptor_unsupported_characters', $unsupported_characters );
return $unsupported_characters;
}
/**
* Sanitizes a string to be used for a statement descriptor.
*
* @since 2.6.19
*
* @link https://stripe.com/docs/connect/statement-descriptors#requirements
*
* @param string $statement_descriptor Statement descriptor to sanitize.
* @return string $statement_descriptor Sanitized statement descriptor.
*/
function edds_sanitize_statement_descriptor( $statement_descriptor ) {
$unsupported_characters = edds_get_statement_descriptor_unsupported_characters();
$statement_descriptor = trim( str_replace( $unsupported_characters, '', $statement_descriptor ) );
$statement_descriptor = substr( $statement_descriptor, 0, 22 );
return $statement_descriptor;
}
/**
* Retrieves a given registry instance by name.
*
* @since 2.6.19
*
* @param string $name Registry name.
* @return null|EDD_Stripe_Registry Null if the registry doesn't exist, otherwise the object instance.
*/
function edds_get_registry( $name ) {
switch( $name ) {
case 'admin-notices':
$registry = EDD_Stripe_Admin_Notices_Registry::instance();
break;
default:
$registry = null;
break;
}
return $registry;
}
/**
* Attempts to verify a nonce from various payment forms when the origin
* of the action isn't explicitly known.
*
* This could be coming from the Checkout, Payment Authorization,
* or Update Payment Method (Recurring) form.
*
* @since 2.8.0
*
* @return bool
*/
function edds_verify_payment_form_nonce() {
if ( false !== edds_verify() ) {
return true;
}
// Update Payment Method.
$nonce = isset( $_POST['edd_recurring_update_nonce'] )
? sanitize_text_field( $_POST['edd_recurring_update_nonce'] )
: '';
if ( ! empty( $nonce ) ) {
return wp_verify_nonce( $nonce, 'update-payment' );
}
return false;
}
/**
* Attempts to verify a given token or nonce.
*
* @since 2.8.12
* @param string $nonce The nonce to check for (default is checkout).
* @param string $action The nonce action (default is checkout).
* @return bool
*/
function edds_verify( $nonce = 'edd-process-checkout-nonce', $action = 'edd-process-checkout' ) {
// Tokenizer.
$token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
$timestamp = isset( $_POST['timestamp'] ) ? sanitize_text_field( $_POST['timestamp'] ) : '';
if ( ! empty( $timestamp ) && ! empty( $token ) ) {
edd_debug_log( $action . ' verified by tokenizer: ' . $token );
return \EDD\Utils\Tokenizer::is_token_valid( $token, $timestamp );
}
edd_debug_log( 'Attempted to verify ' . $action . ' session by tokenizer but the data was missing.' );
// Checkout.
$nonce = isset( $_POST[ $nonce ] )
? sanitize_text_field( $_POST[ $nonce ] )
: '';
if ( empty( $nonce ) ) {
return false;
}
return wp_verify_nonce( $nonce, $action );
}
/**
* Routes user to correct support documentation, depending on whether they are using Standard or Pro version of Stripe
*
* @since 2.8.1
* @param string $type The type of Stripe documentation.
* @return string
*/
function edds_documentation_route( $type ) {
$base_url = 'https://docs.easydigitaldownloads.com/standard';
/**
* Filter to change EDD-Stripe support url.
*
* @since 2.8.1
* @param string $base_url The url string for EDD-Stripe standard.
*/
$base_url = apply_filters( 'edds_documentation_route_base', $base_url );
return trailingslashit( $base_url ) . $type;
}
/**
* Determines if the current Stripe account's country supports application fees.
*
* @since 2.8.7
*
* @return bool True if the Stripe account country (or core "Base Country" setting)
* can use the `application_fee_amount` parameter in API requests.
* True if no country information can be found which will still be
* validated by Stripe when a request is made.
*/
function edds_stripe_connect_account_country_supports_application_fees() {
$edd_country = edd_get_option( 'base_country', '' );
$account_country = edd_get_option(
'stripe_connect_account_country',
$edd_country
);
// If we have no country to compare against try to add an application fee.
// If the Stripe account is actually one of the blocked countries an API
// error will be reflected in the Checkout.
if ( empty( $account_country ) ) {
return true;
}
$account_country = strtolower( $account_country );
$blocked_countries = array(
'br',
);
return ! in_array( $account_country, $blocked_countries, true );
}
/**
* Refunds a charge made via Stripe.
*
* @since 2.8.7
*
* @param int|Order $order_id_or_object The ID or object (EDD 3.0+ only) of the order to refund.
* @param Order|null $refund_object Optional. The refund object associated with this
* charge refund. If provided, then the refund amount
* is used as the charge refund amount (for partial refunds), and
* an EDD transaction record will be inserted (EDD 3.0+ only).
*
* @throws \EDD_Stripe_Utils_Exceptions_Stripe_Object_Not_Found When attempting to call an object or method that is not available.
* @throws \Exception
*/
function edd_refund_stripe_purchase( $order_id_or_object, $refund_object = null ) {
$order_id = $order_id_or_object instanceof Order ? $order_id_or_object->id : $order_id_or_object;
edd_debug_log( sprintf(
'Processing Stripe refund for order #%d',
$order_id
) );
if ( ! is_numeric( $order_id ) ) {
throw new \Exception( __( 'Invalid order ID.', 'easy-digital-downloads' ), 400 );
}
$charge_id = edd_get_payment_transaction_id( $order_id );
if ( empty( $charge_id ) || $charge_id == $order_id ) {
$notes = edd_get_payment_notes( $order_id );
foreach ( $notes as $note ) {
if ( preg_match( '/^Stripe Charge ID: ([^\s]+)/', $note->comment_content, $match ) ) {
$charge_id = $match[1];
break;
}
}
}
// Bail if no charge ID was found.
if ( empty( $charge_id ) ) {
edd_debug_log( sprintf( 'Exiting refund of order #%d. No Stripe charge found.', $order_id ) );
return;
}
$args = array(
'charge' => $charge_id,
);
if ( $refund_object instanceof Order && $order_id_or_object instanceof Order && abs( $refund_object->total ) !== abs( $order_id_or_object->total ) ) {
$args['amount'] = abs( $refund_object->total );
if ( ! edds_is_zero_decimal_currency() ) {
$args['amount'] = round( $args['amount'] * 100, 0 );
}
edd_debug_log( sprintf(
'Processing partial Stripe refund for order #%d. Refund amount: %s; Amount sent to Stripe: %s',
$order_id_or_object->id,
edd_currency_filter( $refund_object->total, $refund_object->currency ),
$args['amount']
) );
} else {
edd_debug_log( sprintf( 'Processing full Stripe refund for order #%d.', $order_id ) );
}
/**
* Filters the refund arguments sent to Stripe.
*
* @link https://stripe.com/docs/api/refunds/create
*
* @param array $args
*/
$args = apply_filters( 'edds_create_refund_args', $args );
/**
* Filters the secondary refund arguments.
*
* @param array $sec_args
*/
$sec_args = apply_filters( 'edds_create_refund_secondary_args', array() );
$refund = edds_api_request( 'Refund', 'create', $args, $sec_args );
$amount_refunded = (float) $refund->amount;
if ( ! edds_is_zero_decimal_currency() ) {
$amount_refunded = round( $amount_refunded / 100, edd_currency_decimal_filter( 2, strtoupper( $refund->currency ) ) );
}
$order_note = sprintf(
/* translators: %1$s the amount refunded; %2$s Stripe Refund ID */
__( '%1$s refunded in Stripe. Refund ID %2$s', 'easy-digital-downloads' ),
edd_currency_filter( $amount_refunded, strtoupper( $refund->currency ) ),
$refund->id
);
edd_insert_payment_note( $order_id, $order_note );
// Add a negative transaction in EDD 3.0+.
if ( $refund_object instanceof Order && function_exists( 'edd_add_order_transaction' ) ) {
edd_add_order_transaction( array(
'object_id' => $refund_object->id,
'object_type' => 'order',
'transaction_id' => sanitize_text_field( $refund->id ),
'gateway' => 'stripe',
'status' => 'complete',
'total' => edd_negate_amount( $amount_refunded )
) );
edd_add_note( array(
'object_id' => $refund_object->id,
'object_type' => 'order',
'user_id' => is_admin() ? get_current_user_id() : 0,
'content' => $order_note
) );
}
/**
* Triggers after a refund has been processed.
*
* @param int $order_id ID of the order that was refunded.
*/
do_action( 'edds_payment_refunded', $order_id );
}
/**
* Checks if Stripe preapproval is enabled. Pro must be active.
*
* @since 2.8.9
* @return bool
*/
function edds_is_preapprove_enabled() {
return edds_is_pro() && edd_get_option( 'stripe_preapprove_only' );
}
/**
* Gets the hidden input for adding tokenizer support to Stripe checkout.
*
* @since 2.8.12
* @param string $custom_id Optional: append a custom string to the ID.
* @return string
*/
function edds_get_tokenizer_input( $custom_id = '' ) {
$id = 'edd-process-stripe-token';
if ( $custom_id ) {
$id .= '-' . $custom_id;
}
$timestamp = time();
return sprintf(
'<input type="hidden" id="%s" data-timestamp="%s" data-token="%s" />',
esc_attr( $id ),
esc_attr( $timestamp ),
esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) )
);
}