';
}
} else {
// Default to each line separate by breaks.
foreach ( $pmpro_price_parts_with_total as $key => $pmpro_price_part ) {
$pmpro_price .= '' . esc_html( $pmpro_price_part['label'] ) . '' . esc_html( $pmpro_price_part['value'] ) . ' ';
}
}
}
return $pmpro_price;
}
/**
* Format a price per the currency settings.
*
* @since 1.7.15
*/
function pmpro_formatPrice( $price ) {
global $pmpro_currency, $pmpro_currency_symbol, $pmpro_currencies;
// start with the rounded price
$formatted = pmpro_round_price( $price );
$decimals = isset( $pmpro_currencies[ $pmpro_currency ]['decimals'] ) ? (int) $pmpro_currencies[ $pmpro_currency ]['decimals'] : pmpro_get_decimal_place();
$decimal_separator = isset( $pmpro_currencies[ $pmpro_currency ]['decimal_separator'] ) ? $pmpro_currencies[ $pmpro_currency ]['decimal_separator'] : '.';
$thousands_separator = isset( $pmpro_currencies[ $pmpro_currency ]['thousands_separator'] ) ? $pmpro_currencies[ $pmpro_currency ]['thousands_separator'] : ',';
$symbol_position = isset( $pmpro_currencies[ $pmpro_currency ]['position'] ) ? $pmpro_currencies[ $pmpro_currency ]['position'] : 'left';
// settings stored in array?
if ( ! empty( $pmpro_currencies[ $pmpro_currency ] ) && is_array( $pmpro_currencies[ $pmpro_currency ] ) ) {
// format number do decimals, with decimal_separator and thousands_separator
$formatted = number_format(
$formatted,
$decimals,
$decimal_separator,
$thousands_separator
);
// which side is the symbol on?
if ( ! empty( $symbol_position ) && $symbol_position == 'left' ) {
$formatted = $pmpro_currency_symbol . $formatted;
} else {
$formatted = $formatted . $pmpro_currency_symbol;
}
} else {
// default to symbol on the left, 2 decimals using . and ,
$formatted = $pmpro_currency_symbol . number_format( $formatted, pmpro_get_decimal_place() );
}
// Trim the trailing zero values.
$formatted = pmpro_trim_trailing_zeroes( $formatted, $decimals, $decimal_separator, $pmpro_currency_symbol, $symbol_position );
// filter
return apply_filters( 'pmpro_format_price', $formatted, $price, $pmpro_currency, $pmpro_currency_symbol );
}
/**
* Filter a sanitized price for display with only the allowed HTML.
*
* @since 2.5.7
*
* @param string $price A price value.
* @return string $price The escaped price with allowed HTML.
*
*/
function pmpro_escape_price( $price ) {
$allowed_price_html = apply_filters(
'pmpro_escape_price_html',
array(
'div' => array (
'class' => array(),
'id' => array(),
),
'span' => array (
'class' => array(),
'id' => array(),
),
'sup' => array (
'class' => array(),
'id' => array(),
),
)
);
return wp_kses( $price, $allowed_price_html );
}
/**
* Function to trim trailing zeros from an amount.
* @since 2.1
* @return float $amount The trimmed amount (removed trailing zeroes).
*/
function pmpro_trim_trailing_zeroes( $amount, $decimals, $decimal_separator, $symbol, $symbol_position = "left" ) {
if ( $decimals <= 2 ) {
return $amount;
}
//Check to see if decimal places are only 0. if so, then don't trim it.
$decimal_value = explode( $decimal_separator, $amount );
if ( empty( $decimal_value[1] ) ) {
return $amount;
}
$is_zero = round( intval( $decimal_value[1] ) );
// Store this in a variable for another time.
$original_amount = $amount;
if ( $is_zero > 0 ) {
if ( $symbol_position == 'right' ) {
$amount = rtrim( $amount, $symbol ); // remove currency symbol.
$amount = rtrim( $amount, 0 ); // remove trailing 0's.
// put the symbol back.
$amount .= $symbol;
} else {
$amount = rtrim( $amount, 0 ); // remove trailing 0's.
}
}
$amount = apply_filters( 'pmpro_trim_cost_amount', $amount, $original_amount, $decimal_separator, $symbol, $symbol_position );
return $amount;
}
/**
* Allow users to adjust the allowed decimal places.
* @since 2.1
*/
function pmpro_get_decimal_place() {
// filter this to support different decimal places.
$decimal_place = apply_filters( 'pmpro_decimal_places', 2 );
if ( intval( $decimal_place ) > 8 ) {
$decimal_place = 8;
}
return $decimal_place;
}
/**
* Which side does the currency symbol go on?
*
* @since 1.7.15
*/
function pmpro_getCurrencyPosition() {
global $pmpro_currency, $pmpro_currencies;
if ( ! empty( $pmpro_currencies[ $pmpro_currency ] ) && is_array( $pmpro_currencies[ $pmpro_currency ] ) && ! empty( $pmpro_currencies[ $pmpro_currency ]['position'] ) ) {
return $pmpro_currencies[ $pmpro_currency ]['position'];
} else {
return 'left';
}
}
/**
* Rounds price based on currency
* Does not format price, to do that, call pmpro_formatPrice().
*
* @param string|float $price to round.
* @param string $currency to round price into.
*/
function pmpro_round_price( $price, $currency = '' ) {
global $pmpro_currency, $pmpro_currencies;
$decimals = pmpro_get_decimal_place();
if ( '' === $currency && ! empty( $pmpro_currencies[ $pmpro_currency ] ) ) {
$currency = $pmpro_currency;
}
if ( ! empty( $pmpro_currencies[ $currency ] )
&& is_array( $pmpro_currencies[ $currency ] )
&& isset( $pmpro_currencies[ $currency ]['decimals'] ) ) {
$decimals = intval( $pmpro_currencies[ $currency ]['decimals'] );
}
$rounded = round( (double) $price, $decimals );
/**
* Filter for result of pmpro_round_price.
*/
$rounded = apply_filters( 'pmpro_round_price', $rounded );
return $rounded;
}
/**
* Rounds price based on currency and returns a string.
*
* Does not format price, to do that, call pmpro_formatPrice().
*
* @since 2.8
*
* @param int|float|string $amount The amount to get price information for.
* @param null|string $currency The currency to use, defaults to current currency.
*
* @return string The rounded price as a string.
*/
function pmpro_round_price_as_string( $amount, $currency = null ) {
$price_info = pmpro_get_price_info( $amount, $currency );
if ( ! $price_info ) {
return (string) pmpro_round_price( $amount, $currency );
}
return $price_info['amount_string'];
}
/**
* Get the price information about the provided amount.
*
* @since 2.8
*
* @param int|float|string $amount The amount to get price information for.
* @param null|string $currency The currency to use, defaults to current currency.
*
* @return array|false The price information about the provided amount.
*/
function pmpro_get_price_info( $amount, $currency = null ) {
if ( ! is_numeric( $amount ) ) {
return false;
}
$amount = (float) $amount;
$currency_info = pmpro_get_currency( $currency );
$price_info = [
// The amount represented as a float.
'amount' => $amount,
// The flat amount represent (example: 1.99 would be 199).
'amount_flat' => 0,
// The amount as a string.
'amount_string' => '',
'parts' => [
// The whole number part of the amount (example: 1.99 would be 1).
'number' => (int) $amount,
// The decimal part of the amount (example: 1.99 would be 99, 1.00 would be 0).
'decimal' => 0,
// The decimal part of the amount as a string (example: 1.99 would be 99, 1.00 would be 00).
'decimal_string' => '',
],
// The currency information.
'currency' => $currency_info,
];
// Enforce integer.
$currency_info['decimals'] = (int) $currency_info['decimals'];
$multiplier = 1;
if ( 0 < $currency_info['decimals'] ) {
$multiplier = pow( 10, $currency_info['decimals'] );
}
// Convert the amount from 100.99 to 10099.
$price_info['amount_flat'] = $amount * $multiplier;
// If there were additional unsupported decimal points, round to remove and convert to integer.
$price_info['amount_flat'] = round( $price_info['amount_flat'] );
// Get the decimal part of the amount as a whole number.
$price_info['parts']['decimal'] = (
$price_info['amount_flat'] - (
$price_info['parts']['number'] * $multiplier
)
);
// Get the zero-padded decimal amount.
$price_info['parts']['decimal_string'] = sprintf( '%02d', $price_info['parts']['decimal'] );
// Get the amount as a string.
$price_info['amount_string'] = sprintf( '%s.%s', $price_info['parts']['number'], $price_info['parts']['decimal_string'] );
return $price_info;
}
/**
* Cast to floats and pad zeroes after the decimal
* when editing the price on the edit level page.
* Only do this for currency with decimals = 2
* Only do this if using . as the decimal separator.
* Only pad zeroes to the decimal portion if there is exactly one number
* after the decimal.
*
* @since 2.0.2
*/
function pmpro_filter_price_for_text_field( $price ) {
global $pmpro_currency, $pmpro_currencies;
// We always want to cast to float
$price = floatval( $price );
// Only do this currencies with 2 decimals
if ( ! empty( $pmpro_currency )
&& is_array( $pmpro_currencies[$pmpro_currency] )
&& isset( $pmpro_currencies[$pmpro_currency]['decimals'] )
&& $pmpro_currencies[$pmpro_currency]['decimals'] != 2 ) {
return $price;
}
// Only do this if using . as the decimal separator.
if ( strpos( $price, '.' ) === false ) {
return $price;
}
$parts = explode( '.', (string)$price );
// If no significant decimals, return the whole number.
if ( empty( $parts[1] ) ) {
return $price;
}
// Do we need an extra 0?
if ( strlen( $parts[1] ) == 1 ) {
$price = (string)$price . '0';
}
return $price;
}
/**
* What gateway should we be using?
*
* @since 1.8
*/
function pmpro_getGateway() {
// grab from param or options
if ( ! empty( $_REQUEST['gateway'] ) ) {
$gateway = sanitize_text_field( $_REQUEST['gateway'] ); // gateway passed as param
} elseif ( ! empty( $_REQUEST['review'] ) ) {
$gateway = 'paypalexpress'; // if review param assume paypalexpress
} else {
$gateway = get_option( 'pmpro_gateway' ); // get from options
}
// set valid gateways - the active gateway in the settings and any gateway added through the filter will be allowed
if ( get_option( 'pmpro_gateway' ) == 'paypal' ) {
$valid_gateways = apply_filters( 'pmpro_valid_gateways', array( 'paypal', 'paypalexpress' ) );
} else {
$valid_gateways = apply_filters( 'pmpro_valid_gateways', array( get_option( 'pmpro_gateway' ) ) );
}
// make sure it's valid
if ( ! in_array( $gateway, $valid_gateways ) ) {
$gateway = false;
}
// filter for good measure
$gateway = apply_filters( 'pmpro_get_gateway', $gateway, $valid_gateways );
return $gateway;
}
/**
* Does the date provided fall in this month.
* Used in logins/visits/views report.
*
* @since 1.8.3
* @param string $str Date to check. Will be passed through strtotime().
*/
function pmpro_isDateThisMonth( $str ) {
$now = current_time( 'timestamp' );
$this_month = intval( date_i18n( 'n', $now ) );
$this_year = intval( date_i18n( 'Y', $now ) );
$date = strtotime( $str, $now );
$date_month = intval( date_i18n( 'n', $date ) );
$date_year = intval( date_i18n( 'Y', $date ) );
if ( $date_month === $this_month && $date_year === $this_year ) {
return true;
} else {
return false;
}
}
/**
* Does the date provided fall within the current week?
* Merged in from the Better Logins Report Add On.
* @since 2.0
* @param string $str Date to check. Will be passed through strtotime().
*/
function pmpro_isDateThisWeek( $str ) {
$now = current_time( 'timestamp' );
$this_week = intval( date( "W", $now ) );
$this_year = intval( date( "Y", $now ) );
$date = strtotime( $str, $now );
$date_week = intval( date( "W", $date ) );
$date_year = intval( date( "Y", $date ) );
if( $date_week === $this_week && $date_year === $this_year ) {
return true;
} else {
return false;
}
}
/**
* Does the dave provided fall within the current year?
* Merged in from the Better Logins Report Add On.
* @since 2.0
* @param string $str Date to check. Will be passed through strtotime().
*/
function pmpro_isDateThisYear( $str ) {
$now = current_time( 'timestamp' );
$this_year = intval( date("Y", $now ) );
$date = strtotime( $str, $now);
$date_year = intval( date("Y", $date ) );
if( $date_year === $this_year ) {
return true;
} else {
return false;
}
}
/**
* Function to generate PMPro front end pages.
*
* @param array $pages {
* Formatted as array($name => $title) or array(array('title'=>'The Title', 'content'=>'The Content'))
*
* @type string $name Page name. (Letters, numbers, and underscores only.)
* @type string $title Page title.
* }
* @return array $created_pages Created page IDs.
* @since 1.8.5
*/
function pmpro_generatePages( $pages ) {
global $pmpro_pages;
$pages_created = array();
if ( ! empty( $pages ) ) {
foreach ( $pages as $name => $page ) {
// does it already exist?
if ( ! empty( $pmpro_pages[ $name ] ) ) {
continue;
}
// no id set. create an array to store the page info
if ( is_array( $page ) ) {
$title = $page['title'];
$content = $page['content'];
} else {
$title = $page;
$content = '[pmpro_' . $name . ']';
}
$insert = array(
'post_title' => $title,
'post_status' => 'publish',
'post_type' => 'page',
'post_content' => $content,
'comment_status' => 'closed',
'ping_status' => 'closed',
);
// make some pages a subpage of account
$post_parent_account_pages = array( 'billing', 'cancel', 'invoice', 'member_profile_edit' );
if ( in_array( $name, $post_parent_account_pages ) ) {
$insert['post_parent'] = $pmpro_pages['account'];
}
// make some pages a subpage of checkout
$post_parent_checkout_pages = array( 'confirmation' );
if ( in_array( $name, $post_parent_checkout_pages ) ) {
$insert['post_parent'] = $pmpro_pages['checkout'];
}
// tweak the login slug
if ( $name == 'login' ) {
$insert['post_name'] = 'login';
}
// create the page
$pmpro_pages[ $name ] = wp_insert_post( $insert );
// update the option too
update_option( 'pmpro_' . $name . '_page_id', $pmpro_pages[ $name ] );
$pages_created[] = $pmpro_pages[ $name ];
}
}
return $pages_created;
}
/**
* Get an array of orders for a specific checkout ID
*
* @param int $checkout_id Checkout ID
* @since 1.8.11
*/
function pmpro_getMemberOrdersByCheckoutID( $checkout_id ) {
global $wpdb;
$order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM $wpdb->pmpro_membership_orders WHERE checkout_id = %d", $checkout_id ) );
$r = array();
foreach ( $order_ids as $order_id ) {
$r[] = new MemberOrder( $order_id );
}
return $r;
}
/**
* Check that the test value is a member of a specific array for sanitization purposes.
*
* @param mixed $needle Value to be tested.
* @param array $safelist Array of safelist values.
* @since 1.9.3
*/
function pmpro_sanitize_with_safelist( $needle, $safelist ) {
if ( ! in_array( $needle, $safelist ) ) {
return false;
} else {
return $needle;
}
}
/**
* Sanitizes the passed value.
* Default sanitizing for things like user fields.
*
* @param array|int|null|string|stdClass $value The value to sanitize
* @param PMPro_Field $field (optional) Field to check type.
*
* @return array|int|string|object Sanitized value
*/
function pmpro_sanitize( $value, $field = null ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $val ) {
$value[ $key ] = pmpro_sanitize( $val );
}
}
if ( is_object( $value ) ) {
foreach ( $value as $key => $val ) {
$value->{$key} = pmpro_sanitize( $val );
}
}
if ( ! empty( $field ) && ! empty( $field->type ) && $field->type === 'textarea' ) {
$value = sanitize_textarea_field( $value );
} elseif ( ( ! is_array( $value ) ) && ctype_alpha( $value ) ||
( ( ! is_array( $value ) ) && strtotime( $value ) ) ||
( ( ! is_array( $value ) ) && is_string( $value ) ) ||
( ( ! is_array( $value ) ) && is_numeric( $value) )
) {
$value = sanitize_text_field( $value );
}
return $value;
}
/**
* Return an array of allowed order statuses
*
* @since 1.9.3
*/
function pmpro_getOrderStatuses( $force = false ) {
global $pmpro_order_statuses;
$statuses = array();
if ( ! isset( $pmpro_order_statuses ) || $force ) {
global $wpdb;
$statuses = array();
$default_statuses = array( '', 'success', 'review', 'token', 'refunded', 'pending', 'error' );
$used_statuses = $wpdb->get_col( "SELECT DISTINCT(status) FROM $wpdb->pmpro_membership_orders" );
$statuses = array_unique( array_merge( $default_statuses, $used_statuses ) );
asort( $statuses );
$statuses = apply_filters( 'pmpro_order_statuses', $statuses );
}
return $statuses;
}
/**
* Cleanup the wp_pmpro_memberships_users_table
* (a) If a user has more than one active row for the same level,
* the older ones are marked inactive.
* (b) If any user has active rows for an non-existent level id,
* those rows are marked as inactive.
*
* @since 1.9.4.4
*/
function pmpro_cleanup_memberships_users_table() {
global $wpdb;
// fix rows for levels that don't exists
$sqlQuery = "UPDATE $wpdb->pmpro_memberships_users mu
LEFT JOIN $wpdb->pmpro_membership_levels l ON mu.membership_id = l.id
SET mu.status = 'inactive'
WHERE mu.status = 'active'
AND l.id IS NULL";
$wpdb->query( $sqlQuery );
// fix rows where there is more than one active status for the same user/level
$sqlQuery = "UPDATE $wpdb->pmpro_memberships_users t1
INNER JOIN (SELECT mu1.id as id
FROM $wpdb->pmpro_memberships_users mu1, $wpdb->pmpro_memberships_users mu2
WHERE mu1.id < mu2.id
AND mu1.user_id = mu2.user_id
AND mu1.membership_id = mu2.membership_id
AND mu1.status = 'active'
AND mu2.status = 'active'
GROUP BY mu1.id
ORDER BY mu1.user_id, mu1.id DESC) t2
ON t1.id = t2.id
SET status = 'inactive'";
$wpdb->query( $sqlQuery );
}
/**
* Are we on the PMPro checkout page?
* @since 2.1
* @return bool True if we are on the checkout page, false otherwise
*/
function pmpro_is_checkout() {
global $pmpro_pages;
// Try is_page first.
if ( ! empty( $pmpro_pages['checkout'] ) ) {
$is_checkout = is_page( $pmpro_pages['checkout'] );
} else {
$is_checkout = false;
}
// Page might not be setup yet or a custom page.
$queried_object = get_queried_object();
if ( ! $is_checkout &&
! empty( $queried_object ) &&
! empty( $queried_object->post_content ) &&
( has_shortcode( $queried_object->post_content, 'pmpro_checkout' ) ||
( function_exists( 'has_block' ) &&
has_block( 'pmpro/checkout-page', $queried_object->post_content )
)
)
) {
$is_checkout = true;
}
/**
* Filter for pmpro_is_checkout return value.
* @since 2.1
* @param bool $is_checkout true if we are on the checkout page, false otherwise
*/
$is_checkout = apply_filters( 'pmpro_is_checkout', $is_checkout );
return $is_checkout;
}
/**
* Are we showing discount codes at checkout?
*/
function pmpro_show_discount_code() {
global $wpdb;
static $show;
// check DB if we haven't yet
if ( !isset( $show ) ) {
if ( $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_discount_codes LIMIT 1" ) ) {
$show = true;
} else {
$show = false;
}
}
$show = apply_filters( "pmpro_show_discount_code", $show );
return $show;
}
/**
* Check if the checkout form was submitted.
* Accounts for image buttons/etc.
* @since 2.1
* @return bool True if the form was submitted, else false.
*/
function pmpro_was_checkout_form_submitted() {
// Default to false.
$submit = false;
// Basic check for a field called submit-checkout.
if ( isset( $_REQUEST['submit-checkout'] ) ) {
$submit = true;
}
// _x stuff in case they clicked on the image button with their mouse
if ( empty( $submit ) && isset( $_REQUEST['submit-checkout_x'] ) ) {
$submit = true;
}
return $submit;
}
/**
* Build the order object used at checkout.
* @since 2.1
* @return mixed $order Order object.
*/
function pmpro_build_order_for_checkout() {
global $post, $gateway, $wpdb, $besecure, $discount_code, $discount_code_id, $pmpro_level, $pmpro_levels, $pmpro_msg, $pmpro_msgt, $pmpro_review, $skip_account_fields, $pmpro_paypal_token, $pmpro_show_discount_code, $pmpro_error_fields, $pmpro_required_billing_fields, $pmpro_required_user_fields, $wp_version, $current_user, $pmpro_requirebilling, $tospage, $username, $password, $password2, $bfirstname, $blastname, $baddress1, $baddress2, $bcity, $bstate, $bzipcode, $bcountry, $bphone, $bemail, $bconfirmemail, $CardType, $AccountNumber, $ExpirationMonth, $ExpirationYear, $pmpro_states, $recaptcha, $recaptcha_privatekey, $CVV;
$morder = new MemberOrder();
$morder->user_id = $current_user->ID;
$morder->membership_id = $pmpro_level->id;
$morder->membership_name = $pmpro_level->name;
$morder->discount_code = $discount_code;
$morder->InitialPayment = pmpro_round_price( $pmpro_level->initial_payment );
$morder->PaymentAmount = pmpro_round_price( $pmpro_level->billing_amount );
$morder->ProfileStartDate = date_i18n( "Y-m-d\TH:i:s", current_time( "timestamp" ) );
$morder->BillingPeriod = $pmpro_level->cycle_period;
$morder->BillingFrequency = $pmpro_level->cycle_number;
if ( $pmpro_level->billing_limit ) {
$morder->TotalBillingCycles = $pmpro_level->billing_limit;
}
if ( pmpro_isLevelTrial( $pmpro_level ) ) {
$morder->TrialBillingPeriod = $pmpro_level->cycle_period;
$morder->TrialBillingFrequency = $pmpro_level->cycle_number;
$morder->TrialBillingCycles = $pmpro_level->trial_limit;
$morder->TrialAmount = pmpro_round_price( $pmpro_level->trial_amount );
}
// Credit card values.
$morder->cardtype = $CardType;
$morder->accountnumber = $AccountNumber;
$morder->expirationmonth = $ExpirationMonth;
$morder->expirationyear = $ExpirationYear;
$morder->ExpirationDate = $ExpirationMonth . $ExpirationYear;
$morder->ExpirationDate_YdashM = $ExpirationYear . "-" . $ExpirationMonth;
$morder->CVV2 = $CVV;
// Not saving email in order table, but the sites need it.
$morder->Email = $bemail;
// Save the user ID if logged in.
if ( $current_user->ID ) {
$morder->user_id = $current_user->ID;
}
// Sometimes we need these split up.
$morder->FirstName = $bfirstname;
$morder->LastName = $blastname;
$morder->Address1 = $baddress1;
$morder->Address2 = $baddress2;
// Set other values.
$morder->billing = new stdClass();
$morder->billing->name = $bfirstname . " " . $blastname;
$morder->billing->street = trim( $baddress1 . " " . $baddress2 );
$morder->billing->city = $bcity;
$morder->billing->state = $bstate;
$morder->billing->country = $bcountry;
$morder->billing->zip = $bzipcode;
$morder->billing->phone = $bphone;
$morder->gateway = $gateway;
$morder->setGateway();
// Set up level var.
$morder->getMembershipLevelAtCheckout();
// Set tax.
$initial_tax = $morder->getTaxForPrice( $morder->InitialPayment );
$recurring_tax = $morder->getTaxForPrice( $morder->PaymentAmount );
// Set amounts.
$morder->initial_amount = pmpro_round_price((float)$morder->InitialPayment + (float)$initial_tax);
$morder->subscription_amount = pmpro_round_price((float)$morder->PaymentAmount + (float)$recurring_tax);
// Filter for order, since v1.8
$morder = apply_filters( 'pmpro_checkout_order', $morder );
return $morder;
}
/**
* Compare a plugin's version to a given version number.
*
* @param string $plugin_file plugin to compare.
* @param string $comparison type of comparison to perform.
* @param string $version version to compare to.
* @return bool
*/
function pmpro_check_plugin_version( $plugin_file, $comparison, $version ) {
// Make sure data to check is in a good format.
if ( empty( $plugin_file ) || empty( $comparison ) || ! isset( $version ) ) {
return false;
}
// Get plugin data.
$full_plugin_file_path = WP_PLUGIN_DIR . '/' . $plugin_file;
if ( is_file( $full_plugin_file_path ) ) {
$plugin_data = get_plugin_data( $full_plugin_file_path, false, true );
}
// Return false if there is no plugin data.
if ( empty( $plugin_data ) || empty( $plugin_data['Version'] ) ) {
return false;
}
// Check version.
if ( version_compare( $plugin_data['Version'], $version, $comparison ) ) {
return true;
} else {
return false;
}
}
/**
* Compare two integers using parameters similar to the version_compare function.
* This allows us to pass in a comparison character via the notification rules
* and get a true/false result.
* @param int $a First integer to compare.
* @param int $b Second integer to compare.
* @param string $operator Operator to use, e.g. >, <, >=, <=, =.
* @return bool true or false based on the operator passed in. Returns null for invalid operators.
*/
function pmpro_int_compare( $a, $b, $operator ) {
switch ( $operator ) {
case '>':
$r = (int)$a > (int)$b;
break;
case '<':
$r = (int)$a < (int)$b;
break;
case '>=':
$r = (int)$a >= (int)$b;
break;
case '<=':
$r = (int)$a <= (int)$b;
break;
case '=':
case '==':
$r = (int)$a == (int)$b;
break;
default:
$r = null;
}
return $r;
}
/**
* Wrapper for $wpdb to insert or replace
* based on the value of the primary key field.
* Using this since using REPLACE on some setups
* results in unexpected behavior.
*
* @since 2.4
*/
function pmpro_insert_or_replace( $table, $data, $format, $primary_key = 'id' ) {
global $wpdb;
if ( empty( $data[$primary_key] ) ) {
// Insert. Remove keys first.
$index = array_search( $primary_key, array_keys( $data ) );
if ( $index !== false ) {
unset( $data[$primary_key] );
unset( $format[$index] );
}
return $wpdb->insert( $table, $data, $format );
} else {
// Replace.
$replaced = $wpdb->replace( $table, $data, $format );
}
}
/**
* Checks if a webhook is running
* @since 2.5
* @param string $gateway If passed in, requires that specific gateway.
* @param bool $set Set to true to set the constant and fire the action hook.
* @return bool True or false if a PMPro webhook set the constant or not.
*/
function pmpro_doing_webhook( $gateway = null, $set = false ){
// If second param is set, set things up.
if ( ! empty( $set ) ) {
define( 'PMPRO_DOING_WEBHOOK', $gateway );
do_action( 'pmpro_doing_webhook', $gateway );
return true;
}
// Otherwise, check if we were already set up.
if( defined( 'PMPRO_DOING_WEBHOOK' ) && !empty ( PMPRO_DOING_WEBHOOK ) ){
if( $gateway !== null ){
if( PMPRO_DOING_WEBHOOK == $gateway ){
return true;
} else {
return false;
}
} else {
return true;
}
} else {
return false;
}
}
/**
* Called once a webhook has been run but was not handled.
*
* @return void
*
* @since 2.8
*/
function pmpro_unhandled_webhook(){
/**
* Allow hooking into after a webhook has been run but was not handled.
*
* @since 2.8
*
* @param string $gateway The gateway the webhook was not handled for.
*/
do_action( 'pmpro_unhandled_webhook', PMPRO_DOING_WEBHOOK );
}
/**
* Sanitizing strings using wp_kses and allowing style tags.
*
* @param string $original_string The string to sanitize.
* @param string $context The sanitization context.
*
* @return string The sanitized string.
*
* @since 2.6.1
*/
function pmpro_kses( $original_string, $context = 'email' ) {
$context = 'pmpro_' . $context;
$sanitized_string = $original_string;
if ( 'pmpro_email' === $context ) {
// Always remove script tags and their contents.
$sanitized_string = preg_replace( '@@si', '', $sanitized_string );
}
$sanitized_string = wp_kses( $sanitized_string, $context );
/**
* Allow overriding the normal pmpro_kses functionality for a context.
*
* @param string $sanitized_string The sanitized string.
* @param string $original_string The original string.
* @param string $context The sanitization context.
*
* @since 2.6.2
*/
return apply_filters( 'pmpro_kses', $sanitized_string, $original_string, $context );
}
/**
* Replace last occurrence of a string.
* From: http://stackoverflow.com/a/3835653/1154321
* @since 2.6
*/
if( ! function_exists("str_lreplace") ) {
function str_lreplace( $search, $replace, $subject ) {
$pos = strrpos( $subject, $search );
if( $pos !== false ) {
$subject = substr_replace( $subject, $replace, $pos, strlen( $search ) );
}
return $subject;
}
}
/**
* Get the last element of an array without affecting the array.
* From: http://www.php.net/manual/en/function.end.php#107733
* @since 2.6
*/
function pmpro_end( $array ) {
return end( $array );
}
/**
* Sort an array of objects by their order property.
* This function is meant to be used with the usort function.
* @since 2.6
*/
function pmpro_sort_by_order( $a, $b ) {
if ( $a->order == $b->order ) {
return 0;
}
return ( $a->order < $b->order ) ? -1 : 1;
}
/**
* Filter the allowed HTML tags for PMPro contexts.
*
* @param array[]|string $allowed_html The allowed HTML tags.
* @param string $context The context name.
*
* @since 2.6.2
*/
function pmpro_kses_allowed_html( $allowed_html, $context ) {
// Only override for our pmpro_* contexts.
if ( 0 !== strpos( $context, 'pmpro_' ) ) {
return $allowed_html;
}
$custom_tags = [];
if ( 'pmpro_email' === $context ) {
$custom_tags['html'] = [
'xmlns' => true,
'xmlns:v' => true,
'xmlns:o' => true,
];
$custom_tags['head'] = [];
$custom_tags['xml'] = [];
$custom_tags['meta'] = [
'name' => true,
'content' => true,
'charset' => true,
'http-equiv' => true,
];
$custom_tags['title'] = [];
$custom_tags['body'] = [];
$custom_tags['table'] = [
'height' => true,
'style' => true,
];
$custom_tags['a'] = [
'style' => true,
];
$custom_tags['style'] = [
'type' => true,
];
}
// Our default context starts with what is available for posts.
$allowed_html = wp_kses_allowed_html( 'post' );
// Merge the allowed HTML tags into our custom tags in case post already has support for it + more.
foreach ( $custom_tags as $tag => $attributes ) {
// Maybe merge our attributes into an already defined tag's attributes.
if ( isset( $allowed_html[ $tag ] ) && true !== $attributes ) {
$attributes = array_merge( $allowed_html[ $tag ], $attributes );
}
$allowed_html[ $tag ] = $attributes;
}
return $allowed_html;
}
add_filter( 'wp_kses_allowed_html', 'pmpro_kses_allowed_html', 10, 2 );
/**
* Show deprecation warning if calling function was called publicly.
*
* Useful for preparing to change method visibility from public to private.
*
* @param string $deprecated_notice_version to show.
* @return bool
*/
function pmpro_method_should_be_private( $deprecated_notice_version ) {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
// Check whether the caller of this function is in the same file (class)
// as the caller of the previous function.
if ( $backtrace[0]['file'] !== $backtrace[1]['file'] ) {
_deprecated_function( esc_html( $backtrace[1]['function'] ), esc_html( $deprecated_notice_version ) );
return true;
}
return false;
}
/**
* Send a 200 HTTP response without ending PHP execution.
*
* Useful to avoid issues like timeouts from gateways during
* webhook/IPN handlers.
*
* Works with Apache and Nginx.
*
* Based on code from https://stackoverflow.com/a/42245266
*
* @since 2.6.4
*/
function pmpro_send_200_http_response() {
/**
* Allow filtering whether to send an early 200 HTTP response.
*
* @since 2.6.4
*
* @param bool $send_early_response Whether to send an early 200 HTTP response.
*/
if ( ! apply_filters( 'pmpro_send_200_http_response', false ) ) {
return;
}
// Ngnix compatibility: Check if fastcgi_finish_request is callable.
if ( is_callable( 'fastcgi_finish_request' ) ) {
session_write_close();
fastcgi_finish_request();
return;
}
ignore_user_abort(true);
ob_start();
$server_protocol = filter_input( INPUT_SERVER, 'SERVER_PROTOCOL', FILTER_SANITIZE_STRING );
if ( ! in_array( $server_protocol, array( 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0' ), true ) ) {
$server_protocol = 'HTTP/1.0';
}
header( $server_protocol . ' 200 OK' );
header( 'Content-Encoding: none' );
header( 'Content-Length: ' . ob_get_length() );
header( 'Connection: close' );
ob_end_flush();
ob_flush();
flush();
}
/**
* Returns formatted ISO-8601 date (Used for Zapier Native app.)
* @since 2.6.6
* @param string $date date A valid date value.
* @return string The date in ISO-8601 format.
* @throws Exception
*/
function pmpro_format_date_iso8601( $date ) {
$datetime = new DateTime( $date );
return $datetime->format( DateTime::ATOM );
}
/**
* Determines the user's actual IP address
*
* $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user
* is making their request through a proxy, or when the web server is behind
* a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather
* than the user's actual address.
*
* Modified from WP_Community_Events::get_unsafe_client_ip() in core WP.
* Modified from https://stackoverflow.com/a/2031935/450127, MIT license.
* Modified from https://github.com/geertw/php-ip-anonymizer, MIT license.
*
* SECURITY WARNING: This function is _NOT_ intended to be used in
* circumstances where the authenticity of the IP address matters. This does
* _NOT_ guarantee that the returned address is valid or accurate, and it can
* be easily spoofed.
*
* @since 2.7
*
* @return string|false The ip address on success or false on failure.
*/
function pmpro_get_ip() {
$client_ip = false;
// In order of preference, with the best ones for this purpose first.
// Added some from JetPack's Jetpack_Protect_Module::get_headers()
$address_headers = array(
'GD_PHP_HANDLER',
'HTTP_AKAMAI_ORIGIN_HOP',
'HTTP_CF_CONNECTING_IP',
'HTTP_CLIENT_IP',
'HTTP_FASTLY_CLIENT_IP',
'HTTP_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_INCAP_CLIENT_IP',
'HTTP_TRUE_CLIENT_IP',
'HTTP_X_CLIENTIP',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_IP_TRAIL',
'HTTP_X_REAL_IP',
'HTTP_X_VARNISH',
'REMOTE_ADDR',
);
foreach ( $address_headers as $header ) {
if ( array_key_exists( $header, $_SERVER ) ) {
/*
* HTTP_X_FORWARDED_FOR can contain a chain of comma-separated
* addresses. The first one is the original client. It can't be
* trusted for authenticity, but we don't need to for this purpose.
*/
$address_chain = explode( ',', sanitize_text_field( $_SERVER[ $header ] ) );
$client_ip = trim( $address_chain[0] );
break;
}
}
if ( ! $client_ip ) {
return false;
}
// Sanitize the IP
$client_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $client_ip );
return $client_ip;
}
/**
* Get the last item of an array without affecting the internal array pointer.
* Going through the function keeps the original array from being updated.
* @since 2.9
* @param $array array The array to get the value of.
* @return mixed Whatever is the last element in the array.
* from: http://www.php.net/manual/en/function.end.php#107733
*/
function pmpro_array_end( $array ) {
return end( $array );
}
/*
* Send the WP new user notification email, but also check our filter.
* Determines if this order can be refunded
* @param object $order The order that we want to refund
* @return bool Returns a bool value based on if the order can be refunded
*/
function pmpro_allowed_refunds( $order ) {
//If this isn't a valid order then lets not allow it
if( empty( $order ) || empty( $order->gateway ) || empty( $order->status ) || empty( $order->payment_transaction_id ) ) {
return false;
}
//Orders with a 0 total shouldn't be able to be refunded
if( $order->total == 0 ){
return false;
}
$okay = false;
/**
* Specify which gateways support refund functionality
*
* @since 2.8
*
* @param array $allowed_gateways A list of allowed gateways to work with refunds
*/
$allowed_gateways = apply_filters( 'pmpro_allowed_refunds_gateways', array( 'stripe', 'paypalexpress' ) );
//Only apply to these gateways
if( in_array( $order->gateway, $allowed_gateways, true ) ) {
$okay = true;
}
$disallowed_statuses = pmpro_disallowed_refund_statuses();
//Don't allow pending orders to be refunded
if( in_array( $order->status, $disallowed_statuses, true ) ){
$okay = false;
}
$okay = apply_filters( 'pmpro_refund_allowed', $okay, $order );
return $okay;
}
/**
* Decides which filter should be used for the refund depending on gateway
* @param object $order Member Order that we are refunding
* @return bool Returns a bool value based on if a refund was processed successfully or not
*/
function pmpro_refund_order( $order ){
if( empty( $order ) ){
return false;
}
//Not going to refund an order that has already been refunded
if( $order->status == 'refunded' ) {
return true;
}
/**
* Processes a refund for a specific gateway
*
* @since 2.8
*
* @param bool $success Default return value is false to determine if the refund was successfully processed.
* @param object $order The Member Order we want to refund.
*/
$success = apply_filters( 'pmpro_process_refund_'.$order->gateway, false, $order );
return $success;
}
/**
* Returns an array of order statuses that do not qualify for a refund
*
* @return array Returns an array of statuses that are not allowed to be refunded
*/
function pmpro_disallowed_refund_statuses() {
/**
* Allow filtering the list of statuses that can not be refunded from.
*
* @since 2.8
*
* @param array $disallowed_statuses The list of statuses that can not be refunded from.
*/
$disallowed_statuses = apply_filters( 'pmpro_disallowed_refund_statuses', array( 'pending', 'refunded', 'review', 'token', 'error' ) );
return $disallowed_statuses;
}
/* Send the WP new user notification email, but also check our filter.
* NOTE: includes/email.php has code to check for the related setting and
* filters on the pmpro_wp_new_user_notification hook.
* @since 2.7.4
* @param int $user_id ID of the user to send the email for.
* @param int $level_id Level ID the user just got. (Need to send to filter.)
*/
function pmpro_maybe_send_wp_new_user_notification( $user_id, $level_id = null ) {
if ( apply_filters( 'pmpro_wp_new_user_notification', true, $user_id, $level_id ) ) {
wp_new_user_notification( $user_id, null, 'both' );
}
}
/**
* Replace all special characters with underscore, including spaces.
*
* @since 2.9
*
* @param string $field_name The raw field name to be formatted.
*/
function pmpro_format_field_name( $field_name ) {
$formatted_name = preg_replace( '/[^A-Za-z0-9\-]+/', '_', $field_name );
/**
* Filter the formatted/output field names.
*
* @since 2.9
*
* @param string $formatted_name The formatted field name (replaced spaces and dashes with underscores).
* @param string $field_name The original field name.
*/
$formatted_name = apply_filters( 'pmpro_formatted_field_name', $formatted_name, $field_name );
return $formatted_name;
}
/**
* Are we activating a plugin?
* @since 2.9.1
* @param string $plugin A specific plugin to check for (optional).
* @return bool True if we are activating a plugin, otherwise false.
*/
function pmpro_activating_plugin( $plugin = null ) {
if ( ! is_admin() ) {
return false;
}
if ( empty( $_REQUEST['action'] ) ) {
return false;
}
if ( $_REQUEST['action'] !== 'activate'
&& $_REQUEST['action'] !== 'activate-selected' ) {
return false;
}
// Not checking for a specific plugin, and activating something.
if ( empty( $plugin ) ) {
return true;
}
// Check if the specified plugin isn't one being activated.
if ( ! empty( $_REQUEST['plugin'] ) && $_REQUEST['plugin'] !== $plugin ) {
return false;
}
if ( ! empty( $_REQUEST['checked'] ) && ! in_array( $plugin, (array)$_REQUEST['checked'] ) ) {
return false;
}
// Must be activating the $plugin specified.
return true;
}
/**
* Compare the stored site URL with the current site URL
*
* @since 2.10
* @return bool True if the stored and current URL match
*/
function pmpro_compare_siteurl() {
$site_url = get_site_url( null, '', 'https' ); // Always get the https version of the site URL.=
$current_url = get_option( 'pmpro_last_known_url' );
// If we don't have a current URL yet, set it to the site URL.
if ( empty( $current_url ) ) {
update_option( 'pmpro_last_known_url', $site_url );
$current_url = $site_url;
}
// We don't want to consider scheme, so just force https for this check.
$site_url = str_replace( 'http://', 'https://', $site_url );
$current_url = str_replace( 'http://', 'https://', $current_url );
return ( $site_url === $current_url );
}
/**
* Determine if the site is in pause mode or not
*
* @since 2.10
* @return bool True if the the site is in pause mode
*/
function pmpro_is_paused() {
// If the current site URL is different than the last known URL, then we are in pause mode.
if ( ! pmpro_compare_siteurl() ) {
return true;
}
// We should never filter this function. We will never change this function to do anything else without lots and lots of discussion and thought.
return false;
}
/**
* Sanitizes a cycle period or expiration period and makes sure it's a valid period.
*
* @since 2.12
*
* @param string $cycle_period The cycle period to sanitize.
* @return string The sanitized cycle period or false if invalid.
*/
function pmpro_sanitize_period( $period ) {
// Validate the cycle period that was passed in.
$allowed_periods = apply_filters( 'pmpro_allowed_periods', array( 'Hour', 'Day', 'Week', 'Month', 'Year' ) );
$sanitized_period = pmpro_sanitize_with_safelist( $period, $allowed_periods );
// If the period was invalid, default to Month.
if ( empty( $sanitized_period ) ) {
$sanitized_period = 'Month';
}
return $sanitized_period;
}
/**
* Set the expiration date for an active membership.
*
* @since 3.0
*
* @param int $user_id The ID of the user to update.
* @param int $level_id The ID of the level to update.
* @param int|string $enddate The date to set the enddate to.
*/
function pmpro_set_expiration_date( $user_id, $level_id, $enddate ) {
global $wpdb;
if ( is_numeric( $enddate ) ) {
$enddate = date( 'Y-m-d H:i:s', $enddate );
}
$wpdb->update(
$wpdb->pmpro_memberships_users,
[
'enddate' => $enddate,
],
[
'status' => 'active',
'membership_id' => $level_id,
'user_id' => $user_id,
],
[
'%s',
],
[
'%s',
'%d',
'%d',
]
);
// Clear the level cache for this user.
pmpro_clear_level_cache_for_user( $user_id );
}
/*
* Check whether a file should be allowed to be uploaded.
*
* By default, only files associated with a user field can be uploaded,
* but there is a filter to allow other files to be uploaded as well.
*
* @since 2.12.4
*
* @param $file_index string The array index of the file to check in the $_FILES array.
* @return true|WP_Error True if the file is allowed, otherwise a WP_Error object.
*/
function pmpro_check_upload( $file_index ) {
global $pmpro_user_fields;
// Check if the file was uploaded.
if ( empty( $_FILES[ $file_index ] ) ) {
return new WP_Error( 'pmpro_upload_error', __( 'No file was uploaded.', 'paid-memberships-pro' ) );
}
// Get the file info.
$file = array_map( 'sanitize_text_field', $_FILES[ $file_index ] );
if ( empty( $file['name'] ) ) {
return new WP_Error( 'pmpro_upload_error', __( 'No file name found.', 'paid-memberships-pro' ) );
}
// If the current user cannot does not have the unfiltered_upload permission, check if the file is an allowed file type.
if ( ! current_user_can( 'unfiltered_upload' ) ) {
$filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
if ( empty( $filetype['ext'] ) || empty( $filetype['type'] ) ) {
return new WP_Error( 'pmpro_upload_error', __( 'Invalid file type.', 'paid-memberships-pro' ) );
}
}
// If this is an upload for a user field, we need to perform additional checks.
$is_user_field = false;
if ( ! empty( $pmpro_user_fields ) && is_array( $pmpro_user_fields ) ) {
foreach ( $pmpro_user_fields as $checkout_box ) {
foreach ( $checkout_box as $field ) {
if ( $field->name == $file_index ) {
// This file is being uploaded for a user field.
$is_user_field = true;
// First, make sure that this is a 'file' field.
if ( $field->type !== 'file' ) {
return new WP_Error( 'pmpro_upload_error', __( 'Invalid field input.', 'paid-memberships-pro' ) );
}
// If there are allowed file types, check if the file is an allowed file type.
// It does not look like the ext property is documented anywhere, but keeping it in case sites are using it.
if ( ! empty( $field->ext ) && is_array( $field->ext ) && ! in_array( $filetype['ext'], $field->ext ) ) {
return new WP_Error( 'pmpro_upload_error', __( 'Invalid file type.', 'paid-memberships-pro' ) );
}
}
}
}
}
if ( ! $is_user_field ) {
/**
* Filter whether a file not associated with a user field can be uploaded.
*
* @since 2.12.4
*
* @param bool $allow_upload True if the file can be uploaded, otherwise false.
* @param array $file The file info.
* @param array $filetype The file type info.
*/
if ( ! apply_filters( 'pmpro_allow_uploading_non_user_field_file', false, $file, $filetype ) ) {
return new WP_Error( 'pmpro_upload_error', __( 'Invalid file submission.', 'paid-memberships-pro' ) );
}
}
// If we made it this far, the file is allowed.
return true;
}