<?php
/****************************************************************

IMPORTANT. PLEASE READ.

DO NOT EDIT THIS FILE or any other file in the /wp-content/plugins/paid-memberships-pro/ directory.
Doing so could break the PMPro plugin and/or keep you from upgrading this plugin in the future.
We regularly release updates to the plugin, including important security fixes and new features.
You want to be able to upgrade.

If you were asked to insert code into "your functions.php file", it was meant that you edit the functions.php
in the root folder of your active theme. e.g. /wp-content/themes/memberlite-child/functions.php
You can also create a custom plugin to place customization code into. Instructions are here:
https://www.paidmembershipspro.com/create-a-plugin-for-pmpro-customizations/

Further documentation for customizing Paid Memberships Pro can be found here:
https://www.paidmembershipspro.com/documentation/
 ****************************************************************/
if ( ! function_exists( 'sornot' ) ) {
	function sornot( $t, $n ) {
		if ( $n == 1 ) {
			return $t;
		} else {
			return $t . 's';
		}
	}
}

// set up wpdb for the tables we need
function pmpro_setDBTables() {
	global $wpdb;
	$wpdb->hide_errors();
	$wpdb->pmpro_membership_levels = $wpdb->prefix . 'pmpro_membership_levels';
	$wpdb->pmpro_memberships_users = $wpdb->prefix . 'pmpro_memberships_users';
	$wpdb->pmpro_memberships_categories = $wpdb->prefix . 'pmpro_memberships_categories';
	$wpdb->pmpro_memberships_pages = $wpdb->prefix . 'pmpro_memberships_pages';
	$wpdb->pmpro_membership_orders = $wpdb->prefix . 'pmpro_membership_orders';
	$wpdb->pmpro_discount_codes = $wpdb->prefix . 'pmpro_discount_codes';
	$wpdb->pmpro_discount_codes_levels = $wpdb->prefix . 'pmpro_discount_codes_levels';
	$wpdb->pmpro_discount_codes_uses = $wpdb->prefix . 'pmpro_discount_codes_uses';
	$wpdb->pmpro_membership_levelmeta = $wpdb->prefix . 'pmpro_membership_levelmeta';
	$wpdb->pmpro_subscriptions = $wpdb->prefix . 'pmpro_subscriptions';
	$wpdb->pmpro_membership_ordermeta = $wpdb->prefix . 'pmpro_membership_ordermeta';
	$wpdb->pmpro_subscriptionmeta = $wpdb->prefix . 'pmpro_subscriptionmeta';
	$wpdb->pmpro_groups = $wpdb->prefix . 'pmpro_groups';
	$wpdb->pmpro_membership_levels_groups = $wpdb->prefix . 'pmpro_membership_levels_groups';
	$wpdb->pmpro_email_log = $wpdb->prefix . 'pmpro_email_log';
}
pmpro_setDBTables();

// thanks: http://wordpress.org/support/topic/is_plugin_active
function pmpro_is_plugin_active( $plugin ) {
	return in_array( $plugin, (array) get_option( 'active_plugins', array() ) );
}

/**
 * @param int $n Override if you have more than 1 group of matches and don't want the first group.
 */
function pmpro_getMatches( $p, $s, $firstvalue = false, $n = 1 ) {
	$ok = preg_match_all( $p, $s, $matches );

	if ( ! $ok ) {
		return false;
	} else {
		if ( $firstvalue ) {
			return $matches[ $n ][0];
		} else {
			return $matches[ $n ];
		}
	}
}

function pmpro_br2nl( $text, $tags = 'br' ) {
	if ( ! is_array( $tags ) ) {
		$tags = explode( ' ', $tags );
	}

	foreach ( $tags as $tag ) {
		$text = preg_replace( "/<{$tag}[^>]*>/", "\n", $text );
		$text = preg_replace( "/<\/{$tag}[^>]*>/", "\n", $text );
	}

	return( $text );
}

/**
 * get_option() should be used directly instead.
 *
 * Will be deprecated in a future release.
 */
function pmpro_getOption( $s, $force = false ) {
	return get_option( 'pmpro_' . $s, '' );
}

function pmpro_setOption( $s, $v = null, $sanitize_function = 'sanitize_text_field', $autoload = false ) {
	if ( $v === null && isset( $_POST[ $s ] ) ) {
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( is_array( $_POST[ $s ] ) ) {
			$v = array_map( $sanitize_function, $_POST[ $s ] );
		} else {
			$v = call_user_func( $sanitize_function, $_POST[ $s ] );
		}
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	}

	if ( is_array( $v ) ) {
		$v = implode( ',', $v );
	} elseif ( is_string( $v ) ) {
		$v = trim( $v );
	}

	return update_option( 'pmpro_' . $s, $v, $autoload );
}

function pmpro_get_slug( $post_id ) {
	global $pmpro_slugs, $wpdb;

	// make sure post id is int for security
	$post_id = intval( $post_id );

	if ( ! $pmpro_slugs[ $post_id ] ) {
		$pmpro_slugs[ $post_id ] = $wpdb->get_var( "SELECT post_name FROM $wpdb->posts WHERE ID = '" . esc_sql( $post_id ) . "' LIMIT 1" );
	}

	return $pmpro_slugs[ $post_id ];
}

function pmpro_url( $page = null, $querystring = '', $scheme = null ) {
	global $besecure;
	$besecure = apply_filters( 'besecure', $besecure );

	if ( ! $scheme && $besecure ) {
		$scheme = 'https';
	} elseif ( ! $scheme ) {
		$scheme = 'http';
	}

	if ( ! $page ) {
		$page = 'levels';
	}

	global $pmpro_pages;

	if ( ! empty( $pmpro_pages[ $page ] ) ) {
		// start with the permalink
		$url = get_permalink( $pmpro_pages[ $page ] );

		// WPML/etc support
		if ( function_exists( 'icl_object_id' ) && defined( 'ICL_LANGUAGE_CODE' ) ) {
			$trans_id = icl_object_id( $pmpro_pages[ $page ], 'page', false, ICL_LANGUAGE_CODE );
			if ( ! empty( $trans_id ) ) {
				$url = get_permalink( $trans_id );
			}
		}
	} else {
		$url = '';
	}

	// figure out querystring
	$querystring = str_replace( '?', '', $querystring );
	parse_str( $querystring, $query_args );

	if ( ! empty( $url ) ) {

		$url = esc_url_raw( add_query_arg( $query_args, $url ) );

		// figure out scheme
		if ( is_ssl() ) {
			$url = str_replace( 'http:', 'https:', $url );
		}
	}

	/**
	 * Filter the URL before returning.
	 */
	$url = apply_filters( 'pmpro_url', $url, $page, $querystring, $scheme );

	return $url;
}

function pmpro_isLevelFree( &$level ) {
	if ( ! empty( $level ) && $level->initial_payment <= 0 && $level->billing_amount <= 0 && $level->trial_amount <= 0 ) {
		$r = true;
	} else {
		$r = false;
	}

	$r = apply_filters( 'pmpro_is_level_free', $r, $level );
	return $r;
}

/**
 * Given an array of levels, will return true if all of them are free.
 */
function pmpro_areLevelsFree( $levelarr ) {
	if ( ! is_array( $levelarr ) ) {
		return false; }
	foreach ( $levelarr as $curlevel ) {
		if ( ! empty( $curlevel ) && ( $curlevel->initial_payment > 0 || $curlevel->billing_amount > 0 || $curlevel->trial_amount > 0 ) ) {
			return false;
		}
	}
	return true;
}

/**
 * Check to see if only free levels are available.
 * @return boolean This will return true if only free levels are available for signup.
 * @internal Creates a filter 'pmpro_only_free_levels'.
 * @since 2.1
 */
function pmpro_onlyFreeLevels() {
	// Get levels that are available for checkout only.
	$levels = pmpro_getAllLevels( false, true );

	return apply_filters( 'pmpro_only_free_levels', pmpro_areLevelsFree( $levels ) );
}

function pmpro_isLevelRecurring( &$level ) {
	if ( ! empty( $level ) && ( $level->billing_amount > 0 || $level->trial_amount > 0 ) ) {
		$r = true;
	} else {
		$r = false;
	}

	$r = apply_filters( 'pmpro_is_level_recurring', $r, $level );
	return $r;
}

/**
 * Check if a user has any recurring levels.
 * Supports Multiple Memberships Per User scenarios.
 * @param $user_id int	User to check for. Defaults to current user.
 * @since 2.2.6
 */
function pmpro_has_recurring_level( $user_id = null ) {
	global $current_user;

	if ( empty( $user_id ) ) {
		$user_id = $current_user->ID;
	}

	if ( empty( $user_id ) ) {
		return false;
	}

	$levels = pmpro_getMembershipLevelsForUser( $user_id );

	if ( empty( $levels ) ) {
		return false;
	}

	foreach( $levels as $level ) {
		if ( pmpro_isLevelRecurring( $level ) ) {
			return true;
		}
	}

	return false;
}

function pmpro_isLevelTrial( &$level ) {
	if ( ! empty( $level ) && ! empty( $level->trial_limit ) && $level->trial_limit > 0 ) {
		$r = true;
	} else {
		$r = false;
	}

	$r = apply_filters( 'pmpro_is_level_trial', $r, $level );
	return $r;
}

function pmpro_isLevelExpiring( &$level ) {
	if ( ! empty( $level ) && ( ! empty( $level->expiration_number ) && $level->expiration_number > 0 ) || ! empty( $level->enddate ) ) {

		$r = true;
	} else {
		$r = false;
	}

	$r = apply_filters( 'pmpro_is_level_expiring', $r, $level );
	return $r;
}

/**
 * Is this level expiring within one pay period
 *
 * @since 1.8.6.3
 *
 * @param object $level PMPro Level Object to test
 */
function pmpro_isLevelExpiringSoon( &$level ) {
	if ( ! pmpro_isLevelExpiring( $level ) || empty( $level->enddate ) ) {
		$r = false;
	} else {
		// days til expiration for the standard level
		$standard = pmpro_getLevel( $level->id );

		if ( ! empty( $standard->expiration_number ) ) {
			if ( $standard->expiration_period == 'Hour' ) {
				$days = $level->expiration_number;
			} else if ( $standard->expiration_period == 'Day' ) {
				$days = $level->expiration_number;
			} elseif ( $standard->expiration_period == 'Week' ) {
				$days = $level->expiration_number * 7;
			} elseif ( $standard->expiration_period == 'Month' ) {
				$days = $level->expiration_number * 30;
			} elseif ( $standard->expiration_period == 'Year' ) {
				$days = $level->expiration_number * 365;
			}
		} else {
			$days = 30;
		}

		// are we within the days til expiration?
		$now = current_time( 'timestamp' );

		if( $standard->expiration_period == 'Hour' ){
			if( $now + ( $days * 60 ) >= $level->enddate ){
				$r = true;
			} else {
				$r = false;
			}
		} else if ( $now + ( $days * 3600 * 24 ) >= $level->enddate ) {
			$r = true;
		} else {
			$r = false;
		}
	}

	// filter
	$r = apply_filters( 'pmpro_is_level_expiring_soon', $r, $level );

	return $r;
}



function pmpro_getLevelCost( &$level, $tags = true, $short = false ) {
	//Bail if no level
	if ( empty( $level ) ) {
		return '';
	}

	// initial payment
	if ( ! $short ) {
		/* translators: %s: initial price */
		$r = sprintf( __( 'The price for membership is <strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ) );
	} else {
		if ( pmpro_isLevelFree( $level ) ) {
			$r = '<strong>' . __('Free', 'paid-memberships-pro' ) . '</strong>';
		} else {
			/* translators: %s: initial price */
			$r = sprintf( __( '<strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ) );
		}
	}

	// recurring part
	if ( (float)$level->billing_amount > 0 ) {
		if ( $level->billing_limit > 1 ) {
			if ( $level->cycle_number == '1' ) {
				/* translators: 1: billing amount, 2: billing period, 3: number of billing periods, 4: plural billing period */
				$r .= sprintf( __( ' and then <strong>%1$s per %2$s for %3$d more %4$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->billing_amount ), pmpro_translate_billing_period( $level->cycle_period ), $level->billing_limit, pmpro_translate_billing_period( $level->cycle_period, $level->billing_limit ) );
			} else {
				/* translators: 1: billing amount, 2: cycle number, 3: billing period, 4: total payments */
				$r .= sprintf( __( ' and then <strong>%1$s every %2$d %3$s for %4$d more payments</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->billing_amount ), $level->cycle_number, pmpro_translate_billing_period( $level->cycle_period, $level->cycle_number ), $level->billing_limit );
			}
		} elseif ( $level->billing_limit == 1 ) {
			/* translators: 1: billing amount, 2: cycle number, 3: billing period */
			$r .= sprintf( __( ' and then <strong>%1$s after %2$d %3$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->billing_amount ), $level->cycle_number, pmpro_translate_billing_period( $level->cycle_period, $level->cycle_number ) );
		} else {
			if ( $level->billing_amount === $level->initial_payment ) {
				if ( $level->cycle_number == '1' ) {
					if ( ! $short ) {
						/* translators: 1: price, 2: billing period */
						$r = sprintf( __( 'The price for membership is <strong>%1$s per %2$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ), pmpro_translate_billing_period( $level->cycle_period ) );
					} else {
						/* translators: 1: price, 2: billing period */
						$r = sprintf( __( '<strong>%1$s per %2$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ), pmpro_translate_billing_period( $level->cycle_period ) );
					}
				} else {
					if ( ! $short ) {
						/* translators: 1: price, 2: cycle number, 3: billing period */
						$r = sprintf( __( 'The price for membership is <strong>%1$s every %2$d %3$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ), $level->cycle_number, pmpro_translate_billing_period( $level->cycle_period, $level->cycle_number ) );
					} else {
						/* translators: 1: price, 2: cycle number, 3: billing period */
						$r = sprintf( __( '<strong>%1$s every %2$d %3$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ), $level->cycle_number, pmpro_translate_billing_period( $level->cycle_period, $level->cycle_number ) );
					}
				}
			} else {
				if ( $level->cycle_number == '1' ) {
					/* translators: 1: billing amount, 2: billing period */
					$r .= sprintf( __( ' and then <strong>%1$s per %2$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->billing_amount ), pmpro_translate_billing_period( $level->cycle_period ) );
				} else {
					/* translators: 1: billing amount, 2: cycle number, 3: billing period */
					$r .= sprintf( __( ' and then <strong>%1$s every %2$d %3$s</strong>.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->billing_amount ), $level->cycle_number, pmpro_translate_billing_period( $level->cycle_period, $level->cycle_number ) );
				}
			}
		}
	} else {
		$r .= '.';
	}

	// add a space
	$r .= ' ';

	// trial part
	if ( $level->trial_limit ) {
		if ( (float)$level->trial_amount == 0 ) {
			if ( $level->trial_limit == '1' ) {
				$r .= ' ' . __( 'After your initial payment, your first payment is Free.', 'paid-memberships-pro' );
			} else {
				/* translators: %d: number of trial payments */
				$r .= ' ' . sprintf( __( 'After your initial payment, your first %d payments are Free.', 'paid-memberships-pro' ), $level->trial_limit );
			}
		} else {
			if ( $level->trial_limit == '1' ) {
				/* translators: %s: the trial payment price */
				$r .= ' ' . sprintf( __( 'After your initial payment, your first payment will cost %s.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->trial_amount ) );
			} else {
				/* translators: 1: number of trial payments, 2: trial payment price */
				$r .= ' ' . sprintf( __( 'After your initial payment, your first %1$d payments will cost %2$s.', 'paid-memberships-pro' ), $level->trial_limit, pmpro_formatPrice( $level->trial_amount ) );
			}
		}
	}

	// taxes part
	$tax_state = get_option( 'pmpro_tax_state' );
	$tax_rate = get_option( 'pmpro_tax_rate' );

	if ( $tax_state && $tax_rate && ! pmpro_isLevelFree( $level ) ) {
		/* translators: 1: tax state, 2: tax rate percentage */
		$r .= sprintf( __( 'Customers in %1$s will be charged %2$s%% tax.', 'paid-memberships-pro' ), $tax_state, round( $tax_rate * 100, 2 ) );
	}

	if ( ! $tags ) {
		$r = strip_tags( $r );
	}

	$r = apply_filters( 'pmpro_level_cost_text', $r, $level, $tags, $short ); // passing $tags and $short since v1.8
	return $r;
}

/**
 * Similar to pmpro_getLevelCost, but loops through all levels in the incoming array and puts it all together.
 */
function pmpro_getLevelsCost( &$levels, $tags = true, $short = false ) {
	// let's build the array to work from to consolidate recurring info.
	// recurpmts[cycle_period][cycle_number][billing_limit] = total_amount
	$initpmt = 0;
	$recurpmts = array();
	$trialperiods = 0;
	foreach ( $levels as $curlevel ) {
		$initpmt += $curlevel->initial_payment;
		if ( (float)$curlevel->billing_amount > 0 ) {
			if ( array_key_exists( $curlevel->cycle_period, $recurpmts ) ) {
				if ( array_key_exists( $curlevel->cycle_number, $recurpmts[ $curlevel->cycle_period ] ) ) {
					if ( array_key_exists( $curlevel->billing_limit, $recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ] ) ) {
						$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ][ $curlevel->billing_limit ] += $curlevel->billing_amount;
					} else {
						$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ][ $curlevel->billing_limit ] = $curlevel->billing_amount;
					}
				} else {
					$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ] = array();
					$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ][ $curlevel->billing_limit ] = $curlevel->billing_amount;
				}
			} else {
				$recurpmts[ $curlevel->cycle_period ] = array();
				$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ] = array();
				$recurpmts[ $curlevel->cycle_period ][ $curlevel->cycle_number ][ $curlevel->billing_limit ] = $curlevel->billing_amount;
			}
		}
		if ( $curlevel->trial_limit && intval( $curlevel->trial_limit ) > $trialperiods ) {
			$trialperiods = intval( $curlevel->trial_limit );
		}
	}

	// initial payment
	if ( ! $short ) {
		/* translators: %s: initial price */
		$r = sprintf( __( 'The price for membership is <strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $initpmt ) );
	} else {
		/* translators: %s: initial price */
		$r = sprintf( __( '<strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $initpmt ) );
	}

	// recurring part
	$billtextparts = array();
	if ( count( $recurpmts ) > 0 ) {
		foreach ( $recurpmts as $curperiod => $curpddata ) {
			foreach ( $curpddata as $curcyclenum => $curcycledata ) {
				foreach ( $curcycledata as $curbilllimit => $curtotal ) {
					if ( $curbilllimit > 1 ) {
						if ( $curcyclenum == '1' ) {
							/* translators: 1: billing amount, 2: billing period, 3: number of billing periods, 4: plural billing period */
							$billtextparts[] = sprintf( __( '<strong>%1$s per %2$s for %3$d more %4$s</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), pmpro_translate_billing_period( $curperiod ), $curbilllimit, pmpro_translate_billing_period( $curperiod, $curbilllimit ) );
						} else {
							/* translators: 1: billing amount, 2: cycle number, 3: billing period, 4: total payments */
							$billtextparts[] = sprintf( __( '<strong>%1$s every %2$d %3$s for %4$d more payments</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), $curcyclenum, pmpro_translate_billing_period( $curperiod, $curcyclenum ), $curbilllimit );
						}
					} elseif ( $curbilllimit == 1 ) {
						/* translators: 1: billing amount, 2: cycle number, 3: billing period */
						$billtextparts[] = sprintf( __( '<strong>%1$s after %2$d %3$s</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), $curcyclenum, pmpro_translate_billing_period( $curperiod, $curcyclenum ) );
					} else {
						if ( $curcyclenum == '1' ) {
							/* translators: 1: billing amount, 2: billing period */
							$billtextparts[] = sprintf( __( '<strong>%1$s every %2$s</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), pmpro_translate_billing_period( $curperiod ) );
						} else {
							/* translators: 1: billing amount, 2: cycle number, 3: billing period */
							$billtextparts[] = sprintf( __( '<strong>%1$s every %2$d %3$s</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), $curcyclenum, pmpro_translate_billing_period( $curperiod, $curcyclenum ) );
						}
					}
				}
			}
		}
		$laststanza = array_pop( $billtextparts );
		if ( count( $billtextparts ) > 0 ) {
			$r .= ', ';
			$r .= implode( ', ', $billtextparts );
		}
		$r .= ', and ' . $laststanza . '.';
	} else {
		$r .= '.';
	}

	// add a space
	$r .= ' ';

	// trial part - not as detailed as the single-level counterpart. Could be improved in the future.
	if ( $trialperiods > 0 ) {
		if ( $trialperiods == 1 ) {
			$r .= __( 'Trial pricing has been applied to the first payment.', 'paid-memberships-pro' );
		} else {
			/* translators: %d: number of trial payments */
			$r .= sprintf( __( 'Trial pricing has been applied to the first %d payments.', 'paid-memberships-pro' ), $trialperiods );
		}
	}

	// taxes part
	$tax_state = get_option( 'pmpro_tax_state' );
	$tax_rate = get_option( 'pmpro_tax_rate' );

	if ( $tax_state && $tax_rate && ! pmpro_areLevelsFree( $levels ) ) {
		/* translators: 1: tax state, 2: tax rate percentage */
		$r .= sprintf( __( 'Customers in %1$s will be charged %2$s%% tax.', 'paid-memberships-pro' ), $tax_state, round( $tax_rate * 100, 2 ) );
	}

	if ( ! $tags ) {
		$r = strip_tags( $r );
	}

	/**
	 * Filter the levels cost text. Note the s in levels. Similar to pmpro_levels_cost_text
	 */
	$r = apply_filters( 'pmpro_levels_cost_text', $r, $levels, $tags, $short );
	return $r;
}

function pmpro_getLevelExpiration( &$level ) {
	//Bail if no level
	if ( empty( $level ) ) {
		return '';
	}

	if ( $level->expiration_number ) {
		$expiration_text = sprintf(
			/* translators: 1: expiration number, 2: expiration period */
			__( 'Membership expires after %1$d %2$s.', 'paid-memberships-pro' ),
			$level->expiration_number,
			pmpro_translate_billing_period( $level->expiration_period, $level->expiration_number )
		);
	} else {
		$expiration_text = '';
	}

	$expiration_text = apply_filters( 'pmpro_levels_expiration_text', $expiration_text, $level );
	$expiration_text = apply_filters( 'pmpro_level_expiration_text', $expiration_text, $level ); // Backwards compatible
	return $expiration_text;
}

function pmpro_getLevelsExpiration( &$levels ) {
	$expirystrings = array();
	$ongoinglevelnum = 0;
	if ( ! empty( $levels ) && ! is_array( $levels ) ) {
		$levels = array( $levels );
	} elseif ( empty( $levels ) ) {
		$levels = array(); }
	foreach ( $levels as $curlevel ) {
		if ( $curlevel->expiration_number ) {
			$expirystrings[] = sprintf(
				/* translators: 1: level name, 2: expiration number, 3: expiration period */
				__( '%1$s membership expires after %2$d %3$s', 'paid-memberships-pro' ),
				$curlevel->name,
				$curlevel->expiration_number,
				pmpro_translate_billing_period( $curlevel->expiration_period, $curlevel->expiration_number )
			);
		} else {
			$ongoinglevelnum++;
		}
	}

	$expiration_text = '';
	if ( count( $expirystrings ) > 0 ) {
		$laststanza = array_pop( $expirystrings );
		$expiration_text = implode( ', ', $expirystrings );
		if ( count( $expirystrings ) > 0 ) {
			$expiration_text .= ', and '; }
		$expiration_text .= $laststanza;
		$expiration_text .= '. ';
		if ( $ongoinglevelnum > 0 ) {
			$expiration_text .= 'The remaining membership';
			if ( $ongoinglevelnum > 1 ) {
				$expiration_text .= 's are';
			} else {
				$expiration_text .= ' is'; }
			$expiration_text .= ' ongoing.';
		}
	}

	/**
	 * Filter the levels expiration text. Note the s in levels. Similar to pmpro_levels_expiration_text
	 */
	$expiration_text = apply_filters( 'pmpro_levels_expiration_text', $expiration_text, $levels );

	// Backwards compatible
	if ( ! empty( $levels ) ) {
		$first_level = reset($levels);
	} else {
		$first_level = false;
	}
	$expiration_text = apply_filters( 'pmpro_level_expiration_text', $expiration_text, $first_level );

	return $expiration_text;
}

/**
 * Get the text to display a membership's expiration date.
 *
 * @since 3.0
 *
 * @param object|int  $level The level object or ID to get the expiration date for.
 * @param WP_User|int $user  The user object or ID to get the expiration date for.
 * @param string|null $default The default text to show when there is no expiration date. If null, a dash is shown.
 *
 * @return string The expiration date text.
 */
function pmpro_get_membership_expiration_text( $level, $user, $default = null ) {
	// If a user ID was passed, get the user object.
	if ( is_numeric( $user ) ) {
		$user = get_userdata( $user );
	}

	// Make sure that we have a user object.
	if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
		return '';
	}

	// If a level ID was passed, get the level object.
	if ( is_numeric( $level ) ) {
		$level = pmpro_getSpecificMembershipLevelForUser( $user->ID, (int)$level );
	}

	// Make sure that we have a level object.
	if ( empty( $level ) || ! is_object( $level ) ) {
		return '';
	}

	/**
	 * Filter to include the expiration time with expiration date.
	 * Used in adminpages/member-edit/pmpro-class-member-edit-panel-memberships.php.
	 *
	 * @param bool $show_time Whether to show the expiration time with expiration date.
	 *
	 * @return bool Whether to show the expiration time with expiration date.
	 */
	$show_time = apply_filters( 'pmpro_show_time_on_expiration_date', false );

	// Generate the expiration date text.
	if ( empty( $level->enddate ) ) {
		// If the level does not have an enddate, show a dash (&#8212;) or empty string.
		$text = ( null === $default ) ? esc_html_x( '&#8212;', 'A dash is shown when there is no expiration date.', 'paid-memberships-pro' ) : $default;
	} elseif ( $show_time ) {
		// Show the enddate with the time.
		$text = sprintf(
			/* translators: %1$s is the date and %2$s is the time. */
			esc_html__( '%1$s at %2$s', 'paid-memberships-pro' ),
			date_i18n( get_option( 'date_format'), $level->enddate ),
			date_i18n( get_option( 'time_format'), $level->enddate )
		);
	} else {
		// Show the enddate without the time.
		$text = date_i18n( get_option( 'date_format' ), $level->enddate );
	}

	// Apply legacy filter pmpro_memberslist_expires_column.
	if ( is_admin() && has_filter( 'pmpro_memberslist_expires_column' ) ) {
		/**
		 * Legacy filter for showing the expiration date in the WP Dashboard.
		 *
		 * Note: Since level data is not passed, this filter is not MMPU-compatible.
		 *
		 * @deprecated 3.0 Use the pmpro_membership_expiration_text filter instead.
		 *
		 * @param string  $text The expiration date text to show for this level.
		 * @param WP_User $user The user that the expiration date is for.
		 *
		 * @return string $text The expiration date text to show for this level.
		 */
		$text = apply_filters_deprecated( 'pmpro_memberslist_expires_column', array( $text, $user ), '3.0', 'pmpro_membership_expiration_text' );
	}

	// Apply legacy filter pmpro_account_membership_expiration_text.
	if ( ! is_admin() && has_filter( 'pmpro_account_membership_expiration_text' ) ) {
		/**
		 * Legacy filter for showing the expiration date on the frontend.
		 *
		 * @deprecated 3.0 Use the pmpro_membership_expiration_text filter instead.
		 *
		 * @param string $text  The expiration date text to show for this level.
		 * @param object $level The level that the expiration date is for.
		 *
		 * @return string $text The expiration date text to show for this level.
		 */
		$text = apply_filters_deprecated( 'pmpro_account_membership_expiration_text', array( $text, $level ), '3.0', 'pmpro_membership_expiration_text' );
	}

	/**
	 * Filter the expiration date text to show for this level.
	 *
	 * @since 3.0
	 *
	 * @param string  $text The expiration date text to show for this level.
	 * @param object  $level The level that the expiration date is for.
	 * @param WP_User $user The user that the expiration date is for.
	 * @param bool    $show_time Whether to show the expiration time with expiration date.
	 *
	 * @return string $text The expiration date text to show for this level.
	 */
	$text = apply_filters( 'pmpro_membership_expiration_text', $text, $level, $user, $show_time );

	return $text;
}

/**
 * pmpro_membership_level Meta Functions
 *
 * @ssince 1.8.6.5
 */
function add_pmpro_membership_level_meta( $level_id, $meta_key, $meta_value, $unique = false ) {
	return add_metadata( 'pmpro_membership_level', $level_id, $meta_key, $meta_value, $unique );
}

function get_pmpro_membership_level_meta( $level_id, $key = '', $single = false ) {
	return get_metadata( 'pmpro_membership_level', $level_id, $key, $single );
}

function update_pmpro_membership_level_meta( $level_id, $meta_key, $meta_value, $prev_value = '' ) {
	return update_metadata( 'pmpro_membership_level', $level_id, $meta_key, $meta_value, $prev_value );
}

function delete_pmpro_membership_level_meta( $level_id, $meta_key, $meta_value = '' ) {
	return delete_metadata( 'pmpro_membership_level', $level_id, $meta_key, $meta_value );
}

/**
 * Get PMPro tables that store non-historical relationships to a membership level.
 *
 * This intentionally excludes orders, subscriptions, and membership history.
 *
 * @since 3.8
 *
 * @return array[] {
 *     An array of table/column pairs.
 *
 *     @type string $table  The table name.
 *     @type string $column The column containing the membership level ID.
 * }
 */
function pmpro_get_membership_level_relationship_tables() {
	global $wpdb;

	return array(
		array(
			'table'  => $wpdb->pmpro_memberships_categories,
			'column' => 'membership_id',
		),
		array(
			'table'  => $wpdb->pmpro_memberships_pages,
			'column' => 'membership_id',
		),
		array(
			'table'  => $wpdb->pmpro_discount_codes_levels,
			'column' => 'level_id',
		),
		array(
			'table'  => $wpdb->pmpro_membership_levels_groups,
			'column' => 'level',
		),
		array(
			'table'  => $wpdb->pmpro_membership_levelmeta,
			'column' => 'pmpro_membership_level_id',
		),
	);
}

/**
 * Delete non-historical records related to a membership level.
 *
 * @since 3.8
 *
 * @param int $level_id The membership level ID.
 * @return bool True if all related records were deleted or no related records existed; false on database error.
 */
function pmpro_delete_membership_level_relationships( $level_id ) {
	global $wpdb;

	$level_id = intval( $level_id );
	if ( empty( $level_id ) ) {
		return false;
	}

	$success = true;
	foreach ( pmpro_get_membership_level_relationship_tables() as $relationship_table ) {
		$deleted = $wpdb->delete(
			$relationship_table['table'],
			array( $relationship_table['column'] => $level_id ),
			array( '%d' )
		);

		if ( false === $deleted ) {
			$success = false;
		}
	}

	wp_cache_delete( $level_id, 'pmpro_membership_level_meta' );

	return $success;
}

/**
 * Delete orphaned non-historical membership level relationship records.
 *
 * @since 3.8
 *
 * @return bool True if all orphaned records were deleted or no orphaned records existed; false on database error.
 */
function pmpro_delete_orphaned_membership_level_relationships() {
	global $wpdb;

	$success = true;
	foreach ( pmpro_get_membership_level_relationship_tables() as $relationship_table ) {
		$orphaned_level_ids = array();
		if ( $relationship_table['table'] === $wpdb->pmpro_membership_levelmeta ) {
			$orphaned_level_ids = (array) $wpdb->get_col(
				"SELECT DISTINCT pmpro_level_relationship.`{$relationship_table['column']}`
				FROM {$relationship_table['table']} AS pmpro_level_relationship
				LEFT JOIN {$wpdb->pmpro_membership_levels} AS pmpro_membership_level
					ON pmpro_level_relationship.`{$relationship_table['column']}` = pmpro_membership_level.id
				WHERE pmpro_membership_level.id IS NULL"
			);
		}

		$deleted = $wpdb->query(
			"DELETE pmpro_level_relationship
			FROM {$relationship_table['table']} AS pmpro_level_relationship
			LEFT JOIN {$wpdb->pmpro_membership_levels} AS pmpro_membership_level
				ON pmpro_level_relationship.`{$relationship_table['column']}` = pmpro_membership_level.id
			WHERE pmpro_membership_level.id IS NULL"
		);

		if ( false === $deleted ) {
			$success = false;
		}

		foreach ( $orphaned_level_ids as $level_id ) {
			wp_cache_delete( intval( $level_id ), 'pmpro_membership_level_meta' );
		}
	}

	return $success;
}

/**
 * pmpro_membership_order Meta Functions
 */
function add_pmpro_membership_order_meta( $order_id, $meta_key, $meta_value, $unique = false ) {
	return add_metadata( 'pmpro_membership_order', $order_id, $meta_key, $meta_value, $unique );
}

function get_pmpro_membership_order_meta( $order_id, $key = '', $single = false ) {
	return get_metadata( 'pmpro_membership_order', $order_id, $key, $single );
}

function update_pmpro_membership_order_meta( $order_id, $meta_key, $meta_value, $prev_value = '' ) {
	return update_metadata( 'pmpro_membership_order', $order_id, $meta_key, $meta_value, $prev_value );
}

function delete_pmpro_membership_order_meta( $order_id, $meta_key, $meta_value = '' ) {
	return delete_metadata( 'pmpro_membership_order', $order_id, $meta_key, $meta_value );
}

/**
 * pmpro_subscription Meta Functions
 */
function add_pmpro_subscription_meta( $subscription_id, $meta_key, $meta_value, $unique = false ) {
	return add_metadata( 'pmpro_subscription', $subscription_id, $meta_key, $meta_value, $unique );
}

function get_pmpro_subscription_meta( $subscription_id, $key = '', $single = false ) {
	return get_metadata( 'pmpro_subscription', $subscription_id, $key, $single );
}

function update_pmpro_subscription_meta( $subscription_id, $meta_key, $meta_value, $prev_value = '' ) {
	return update_metadata( 'pmpro_subscription', $subscription_id, $meta_key, $meta_value, $prev_value );
}

function delete_pmpro_subscription_meta( $subscription_id, $meta_key, $meta_value = '' ) {
	return delete_metadata( 'pmpro_subscription', $subscription_id, $meta_key, $meta_value );
}

function pmpro_hideAds() {
	global $pmpro_display_ads;
	return ! $pmpro_display_ads;
}

function pmpro_displayAds() {
	global $pmpro_display_ads;
	return $pmpro_display_ads;
}

/**
 * Get the next payment date for a user.
 *
 * This function will only return the next payment date for the subscription with
 * the most recent start date. For this reason, it is not MMPU-compatible.
 *
 * @since unknown
 *
 * @param int|null $user_id      User ID. Defaults to the current user.
 * @param string   $order_status If value passed in is not "success", will get next payment date for a cancelled subscription.
 * @param string   $format       Date format to return.
 *
 * @return string|bool Date of next payment, or false if no subscription is found.
 */
function pmpro_next_payment( $user_id = null, $order_status = 'success', $format = 'timestamp' ) {
	global $wpdb, $current_user;
	if ( ! $user_id ) {
		$user_id = $current_user->ID;
	}

	if ( $user_id ) {
		// Convert passed order status to a subscription status.
		$subscription_status = ( $order_status === 'success' || ( is_array( $order_status ) && in_array( 'success', $order_status ) ) ) ? 'active' : 'cancelled';
		$subscriptions = PMPro_Subscription::get_subscriptions_for_user( $user_id, null, $subscription_status );
		if ( ! empty( $subscriptions ) ) {
			return $subscriptions[0]->get_next_payment_date( $format );
		}
	}
	return false;
}

if ( ! function_exists( 'last4' ) ) {
	function last4( $t ) {
		return substr( $t, strlen( $t ) - 4, 4 );
	}
}

if ( ! function_exists( 'hideCardNumber' ) ) {
	function hideCardNumber( $c, $dashes = true ) {
		if ( $c ) {
			if ( $dashes ) {
				return 'XXXX-XXXX-XXXX-' . substr( $c, strlen( $c ) - 4, 4 );
			} else {
				return 'XXXXXXXXXXXX' . substr( $c, strlen( $c ) - 4, 4 );
			}
		} else {
			return '';
		}
	}
}

// check for existing functions since we didn't use a prefix for this function
if ( ! function_exists( 'cleanPhone' ) ) {
	/**
	 * Function to remove special characters from a phone number.
	 * NOTE: Could probably replace with preg_replace("[^0-9]", "", $phone)
	 *
	 * @since 1.0
	 *
	 * @param string $phone The phone number to clean.
	 */
	function cleanPhone( $phone ) {
		// Return an empty string if no phone number is provided.
		if ( empty( $phone ) ) {
			return '';
		}
		// if a + is passed, just pass it along
		if ( strpos( $phone, '+' ) !== false ) {
			return $phone;
		}
		// clean the phone
		$phone = str_replace( '-', '', $phone );
		$phone = str_replace( '.', '', $phone );
		$phone = str_replace( '(', '', $phone );
		$phone = str_replace( ')', '', $phone );
		$phone = str_replace( ' ', '', $phone );
		return $phone;
	}
}

// check for existing functions since we didn't use a prefix for this function
if ( ! function_exists( 'formatPhone' ) ) {
	/**
	 * Function to format a phone number.
	 *
	 * @since 1.0
	 *
	 * @param string $phone The phone number to format.
	 */
	function formatPhone( $phone ) {
		$r = cleanPhone( $phone );

		if ( strlen( $r ) == 11 ) {
			$r = substr( $r, 0, 1 ) . ' (' . substr( $r, 1, 3 ) . ') ' . substr( $r, 4, 3 ) . '-' . substr( $r, 7, 4 );
		} elseif ( strlen( $r ) == 10 ) {
			$r = '(' . substr( $r, 0, 3 ) . ') ' . substr( $r, 3, 3 ) . '-' . substr( $r, 6, 4 );
		} elseif ( strlen( $r ) == 7 ) {
			$r = substr( $r, 0, 3 ) . '-' . substr( $r, 3, 4 );
		}

		/**
		 * Filter to do more or less cleaning of phone numbers.
		 *
		 * @since 1.8.4.4
		 *
		 * @param string $r The formatted phone number.
		 * @param string $phone The original phone number.
		 */
		return apply_filters( 'pmpro_format_phone', $r, $phone );
	}
}

/**
 * Display a message to users based on their current status.
 *
 * @since 2.4.5
 * @deprecated 3.1
 */
function pmpro_showRequiresMembershipMessage() {
	_deprecated_function( __FUNCTION__, '3.1' );
	global $current_user, $post_membership_levels_names;

	// get the correct message
	if ( is_feed() ) {
		$content = get_option( 'pmpro_rsstext' );
		$content = str_replace( '!!levels!!', implode( ', ', $post_membership_levels_names ), $content );
	} elseif ( $current_user->ID ) {
		// not a member
		$content = get_option( 'pmpro_nonmembertext' );
		$content = str_replace( '!!levels!!', implode( ', ', $post_membership_levels_names ), $content );
	} else {
		// not logged in!
		$content = get_option( 'pmpro_notloggedintext' );
		$content = str_replace( '!!levels!!', implode( ', ', $post_membership_levels_names ), $content );
	}
}

/**
 * Function to check if a user has specified membership levels.
 *
 * pmpro_hasMembershipLevel() checks if the passed user is a member of the passed level
 * $level may either be the ID or name of the desired membership_level. (or an array of such or a comma separated string)
 * If $user_id is omitted, the value will be retrieved from $current_user.
 *
 *  Return values:
 *   * Success returns boolean true.
 *   * Failure returns a string containing the error message.
 *
 * @since 2.12.3 Added support to pass comma separated value to $levels
 * @since 1.8.5 Added 'e' option for expired members.
 * @since 1.0.0
 *
 * @param mixed $levels The levels to check.
 * @param int   $user_id The user ID to check.
 *
 * @return bool Result of membership query.
 */
function pmpro_hasMembershipLevel( $levels = null, $user_id = null ) {
	global $current_user, $wpdb;

	$return = false;

	if ( empty( $user_id ) ) {
		$user_id = $current_user->ID;
	}

	if ( ! empty( $user_id ) && is_numeric( $user_id ) ) { // get membership levels for given user
		$membership_levels = pmpro_getMembershipLevelsForUser( $user_id );
	} else {
		$membership_levels = null; // non-users don't have levels
	}

	if ( $levels === '0' || $levels === 0 ) {
		$return = empty( $membership_levels );
	} elseif ( empty( $levels ) ) {
		$return = ! empty( $membership_levels );
	} else {
		if ( ! is_array( $levels ) ) {
			// Check for a comma.
			$levels_str = (string)$levels;
			if ( strpos( $levels_str, ',' ) !== false ) {
				// We have a string with at least 1 comma in it, turn it into an array.
				$level_ids = explode( ',', $levels_str );
				// Trim whitespace from the levels ids or names.
				$levels = array_map( 'trim', $level_ids );
			} else {
				// No comma, but we want an array of levels.
				$levels = array( $levels );
			}
		}

		if ( empty( $membership_levels ) ) {
			// check for negative level
			$negative_level = false;
			foreach ( $levels as $level ) {
				if ( intval( $level ) < 0 ) {
					$negative_level = true;
					break;
				}
			}

			// are we looking for non-members or not?
			if ( $negative_level ) {
				return true;                                                        // -1/etc, negative level
			} elseif ( in_array( 0, $levels, true ) || in_array( '0', $levels ) ) {
				$return = true;                                                     // 0 level
			} elseif ( in_array( 'L', $levels ) || in_array( 'l', $levels ) ) {
				$return = ( ! empty( $user_id ) && $user_id == $current_user->ID );      // L, logged in users
			} elseif ( in_array( '-L', $levels ) || in_array( '-l', $levels ) ) {
				$return = ( empty( $user_id ) || $user_id != $current_user->ID );       // -L, not logged in users
			} elseif ( in_array( 'E', $levels ) || in_array( 'e', $levels ) ) {
				$sql = "SELECT id FROM $wpdb->pmpro_memberships_users WHERE user_id = " . (int) $user_id . " AND status ='expired' LIMIT 1";
				$expired = $wpdb->get_var( $sql );                                    // E, expired members
				$return = ! empty( $expired );
			}
		} else {
			foreach ( $levels as $level ) {
				if ( strtoupper( $level ) == 'L' ) {
					// checking if user is logged in
					if ( ! empty( $user_id ) && $user_id == $current_user->ID ) {
						$return = true;
					}
				} elseif ( strtoupper( $level ) == '-L' ) {
					// checking if user is logged out
					if ( empty( $user_id ) || $user_id != $current_user->ID ) {
						$return = true;
					}
				} elseif ( $level === '0' || $level === 0 || strtoupper( $level ) === 'E' ) {
					continue;   // user with levels so not a "non-member" or expired
				} else {
					// checking a level id
					$level_obj = pmpro_getLevel( is_numeric( $level ) ? abs( intval( $level ) ) : $level ); // make sure our level is in a proper format
					if ( empty( $level_obj ) ) {
						continue;} //invalid level
					$found_level = false;

					foreach ( $membership_levels as $membership_level ) {
						if ( $membership_level->id == $level_obj->id || $membership_level->name == $level_obj->name) {
							$found_level = true;
						}
					}

					if ( is_numeric( $level ) && intval( $level ) < 0 && ! $found_level ) {
						$return = true;
					} elseif ( is_numeric( $level ) && intval( $level ) > 0 && $found_level ) {
						$return = true;
					} elseif ( ! is_numeric( $level ) && $found_level) { // if a level name was passed
						$return = true;
					}
				}
			}
		}
	}

	$return = apply_filters( 'pmpro_has_membership_level', $return, $user_id, $levels );
	return $return;
}

/**
 * Remove a membership level from a user.
 *
 * @since 1.8.11
 *
 * @param int $level_id ID of the level to remove.
 * @param int $user_id ID of the user to remove the level from.
 * @param string $status Status to set the membership to.
 *
 * @return bool True if the level was removed, false otherwise.
 */
function pmpro_cancelMembershipLevel( $level_id, $user_id = null, $status = 'inactive' ) {
	global $current_user, $wpdb, $pmpro_error;

	// If we weren't passed a user ID, use the current user.
	if ( empty( $user_id ) && ! empty( $current_user->ID ) ) {
		$user_id = $current_user->ID;
	} elseif ( empty( $user_id ) ) {
		$pmpro_error = __( 'User ID not found.', 'paid-memberships-pro' );
		return false;
	}

	// If the user doesn't have the level, we don't need to do anything.
	$user_levels = pmpro_getMembershipLevelsForUser( $user_id );
	$user_level_ids = array_map( 'intval', wp_list_pluck( $user_levels, 'id' ) );
	if ( ! in_array( (int)$level_id, $user_level_ids ) ) {
		return false;
	}

	// Set old user levels to be used in the pmpro_do_action_after_all_membership_level_changes() function.
	pmpro_set_old_user_levels( $user_id );

	// If we are not giving the user a new level, run the before change membership action.
	if ( ! in_array( $status, array( 'changed', 'admin_changed' ) ) ) {
		/**
		 * Action to run before the membership level changes.
		 *
		 * @param int $level_id ID of the level changed to.
		 * @param int $user_id ID of the user changed.
		 * @param int $cancel_level ID of the level being cancelled if specified.
		 */
		do_action( 'pmpro_before_change_membership_level', 0, $user_id, pmpro_getMembershipLevelsForUser( $user_id ), $level_id );
	}

	// Remove the membership level.
	$cols_set = array( 'status'=> $status, 'enddate' => current_time( 'mysql' ) );
	$cols_where = array( 'user_id' => $user_id, 'membership_id' => $level_id, 'status' => 'active' );
	$cols_format = array( '%s', '%s');
	if ( $wpdb->update( $wpdb->pmpro_memberships_users, $cols_set, $cols_where, $cols_format ) === false ) {
		$pmpro_error = __( 'Error interacting with database', 'paid-memberships-pro' ) . ': ' . ( $wpdb->last_error ? $wpdb->last_error : 'unavailable' );
		return false;
	}

	// Check if we should cancel the user's subscription.
	if ( apply_filters( 'pmpro_cancel_previous_subscriptions', true ) ) {
		$active_subscriptions = PMPro_Subscription::get_subscriptions_for_user( $user_id, $level_id );
		foreach ( $active_subscriptions as $subscription ) {
			$subscription->cancel_at_gateway();
		}
	}

	// If we are not giving the user a new level, clear the level cache for this user and run the change membership action.
	if ( ! in_array( $status, array( 'changed', 'admin_changed' ) ) ) {
		// Clear the level cache for this user.
		pmpro_clear_level_cache_for_user( $user_id );

		/**
		 * Action to run after the membership level changes.
		 *
		 * @param int $level_id ID of the level changed to.
		 * @param int $user_id ID of the user changed.
		 * @param int $cancel_level ID of the level being cancelled if specified.
		 */
		do_action( 'pmpro_after_change_membership_level', 0, $user_id, $level_id );
	}

	return true;
}

/**
 * Give a membership level to a user.
 *
 * If $user_id is omitted, the value will be retrieved from $current_user.
 *
 * @param int|array $level ID of level to set as new level, use 0 to cancel membership
 * @param int       $user_id ID of the user to change levels for
 * @param string    $old_level_status Deprecated. The status to set for the row in the memberships users table. (e.g. inactive, cancelled, admin_cancelled, expired) Defaults to 'inactive'.
 * @param int       $cancel_level Deprecated. If set cancel just this one level instead of all active levels (to support Multiple Memberships per User)
 *
 * @return bool|void
 * Return values:
 *      Success returns boolean true.
 *      Failure returns boolean false.
 *		No change returns null.
 */
function pmpro_changeMembershipLevel( $level, $user_id = null, $old_level_status = 'inactive', $cancel_level = null ) {
	global $current_user, $pmpro_error, $wpdb, $pmpro_giving_level;

	if ( empty( $user_id ) ) {
		$user_id = $current_user->ID;
	}

	if ( empty( $user_id ) ) {
		$pmpro_error = __( 'User ID not found.', 'paid-memberships-pro' );
		return false;
	}

	// make sure user id is int for security
	$user_id = intval( $user_id );

	/**
	 * Filter the level passed in.
	 * @since 2.5.8
	 */
	$level = apply_filters( 'pmpro_change_level', $level, $user_id, $old_level_status, $cancel_level );

	// Check if we are trying to cancel a single level (Deprecated).
	if ( empty( $level ) && ! empty( $cancel_level ) ) {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'The $cancel_level parameter is deprecated. Use pmpro_cancelMembershipLevel() instead.', 'paid-memberships-pro' ), '3.0' );
		return pmpro_cancelMembershipLevel( $cancel_level, $user_id, $old_level_status );
	}

	// Check if we are trying to cancel all levels (Deprecated).
	if ( empty( $level ) && empty( $cancel_level ) ) {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'The pmpro_cancelMembershipLevel() function should be used to cancel membership levels.', 'paid-memberships-pro' ), '3.0' );
		$membership_levels = pmpro_getMembershipLevelsForUser( $user_id );
		$success = true;
		foreach ( $membership_levels as $membership_level ) {
			$deletion_success = pmpro_cancelMembershipLevel( $membership_level->id, $user_id, $old_level_status );
			if ( ! $deletion_success ) {
				$success = false;
			}
		}
		return $success;
	}

	// Now we know that we are actually adding a level.
	// Check if they they already have the membership level that we are changing them to.
	if ( ! is_array( $level ) && pmpro_hasMembershipLevel( $level, $user_id ) ) {
		return;
	}

	// Get the level ID.
	if ( is_array( $level ) ) {
		// We have a custom level. Make sure that it is valid.
		if ( empty( $level['membership_id'] ) ) {
			$pmpro_error = __( 'No membership_id specified in pmpro_changeMembershipLevel().', 'paid-memberships-pro' );
			return false;
		}
		$level_id = (int) $level['membership_id'];
	} elseif ( is_numeric( $level ) ) {
		// Only a level ID was passed.
		$level_id = (int) $level;
	} else {
		// Invalid level passed.
		$pmpro_error = __( 'Invalid level parameter passed to pmpro_changeMembershipLevel().', 'paid-memberships-pro' );
		return false;
	}

	// Make sure that level actually exists.
	if ( empty( pmpro_getLevel( $level_id ) ) ) {
		$pmpro_error = __( 'Invalid level.', 'paid-memberships-pro' );
		return false;
	}

	// Set old user levels to be used in the pmpro_do_action_after_all_membership_level_changes() function.
	pmpro_set_old_user_levels( $user_id );

	// Get all levels for the user.
	$membership_levels = pmpro_getMembershipLevelsForUser( $user_id );
	$membership_ids    = wp_list_pluck( $membership_levels, 'id' );

	/**
	 * Action to run before the membership level changes.
	 *
	 * @param int $level_id ID of the level changed to.
	 * @param int $user_id ID of the user changed.
	 * @param array $old_levels array of prior levels the user belonged to.
	 * @param int $cancel_level ID of the level being cancelled if specified
	 */
	do_action( 'pmpro_before_change_membership_level', $level_id, $user_id, $membership_levels, null );

	// Cancel all membership levels for this user in the same level group if only one level per group is allowed.
	$level_group_id = pmpro_get_group_id_for_level( $level_id );
	$level_group = pmpro_get_level_group( $level_group_id );

	/**
	 * Filter whether old levels should be deactivated or not.
	 * Typically you'll want to hook into pmpro_before_change_membership_level
	 * or pmpro_after_change_membership_level later to run your own deactivation logic.
	 *
	 * @since  1.8.11
	 * @var $pmpro_deactivate_old_levels bool True or false if levels should be deactivated. Defaults to true.
	 */
	$pmpro_deactivate_old_levels = apply_filters( 'pmpro_deactivate_old_levels', true );

	// If we are deactivating old levels, typically we will want to put the old level in 'changed status.
	// The exception is if the level is being changed by an administrator, in which case 'admin_changed' would have been passed to this function.
	$change_status = 'admin_changed' === $old_level_status ? 'admin_changed' : 'changed';
	if ( ! empty( $level_group) && empty( $level_group->allow_multiple_selections ) && $pmpro_deactivate_old_levels ) {
		// Get all levels in the group.
		$levels_in_group = pmpro_get_levels_for_group( $level_group->id );
		$group_level_ids = wp_list_pluck( $levels_in_group, 'id' );

		// Get the intersection of the two arrays.
		$levels_to_cancel = array_intersect( $group_level_ids, $membership_ids );

		// Cancel the levels.
		foreach ( $levels_to_cancel as $level_to_cancel ) {
			pmpro_cancelMembershipLevel( $level_to_cancel, $user_id, $change_status );
		}
	} elseif ( $pmpro_deactivate_old_levels ) {
		// If the user already has this membership level, we still want to cancel it.
		if ( in_array( $level_id, $membership_ids ) ) {
			pmpro_cancelMembershipLevel( $level_id, $user_id, $change_status );
		}
	}

	// Insert current membership
	if ( ! empty( $level ) ) {
		// make sure the dates are in good formats
		if ( is_array( $level ) ) {
			// Better support mySQL Strict Mode by passing  a proper enum value for cycle_period
			if ( $level['cycle_period'] == '' ) {
				$level['cycle_period'] = 0; }

			// clean up date formatting (string/not string)
			$level['startdate'] = preg_replace( '/\'/', '', $level['startdate'] );
			$level['enddate'] = preg_replace( '/\'/', '', $level['enddate'] );

			$sql = $wpdb->prepare(
				"
					INSERT INTO {$wpdb->pmpro_memberships_users}
					(`user_id`, `membership_id`, `code_id`, `initial_payment`, `billing_amount`, `cycle_number`, `cycle_period`, `billing_limit`, `trial_amount`, `trial_limit`, `startdate`, `enddate`)
					VALUES
					( %d, %d, %d, %s, %s, %d, %s, %d, %s, %d, %s, %s )",
				$level['user_id'], // integer
				$level['membership_id'], // integer
				$level['code_id'], // integer
				$level['initial_payment'], // float (string)
				$level['billing_amount'], // float (string)
				$level['cycle_number'], // integer
				$level['cycle_period'], // string (enum)
				$level['billing_limit'], // integer
				$level['trial_amount'], // float (string)
				$level['trial_limit'], // integer
				$level['startdate'], // string (date)
				$level['enddate'] // string (date)
			);
		} else {
			$sql = $wpdb->prepare(
				"
				INSERT INTO {$wpdb->pmpro_memberships_users}
				( `user_id`, `membership_id`, `code_id`, `initial_payment`, `billing_amount`, `cycle_number`, `cycle_period`, `billing_limit`, `trial_amount`, `trial_limit`, `startdate`, `enddate`)
					VALUES
					( %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %s, %s )",
				$user_id,
				$level_id,
				'0',
				'0',
				'0',
				'0',
				'0',
				'0',
				'0',
				'0',
				current_time( 'mysql' ),
				'0000-00-00 00:00:00'
			);
		}

		if ( false === $wpdb->query( $sql ) ) {
			/* translators: %s: the database error message */
			$pmpro_error = sprintf( __( 'Error interacting with database: %s', 'paid-memberships-pro' ), ( ! empty( $wpdb->last_error ) ? $wpdb->last_error : 'unavailable' ) );
			return false;
		}

		/**
		 * Allow filtering whether to remove duplicate "active" memberships by setting them to "changed".
		 *
		 * @since 2.6.6
		 *
		 * @param bool $remove_duplicate_memberships Whether to remove duplicate "active" memberships by setting them to "changed".
		 */
		$remove_duplicate_memberships = apply_filters( 'pmpro_remove_duplicate_membership_entries', true );

		if ( $remove_duplicate_memberships ) {
			$wpdb->query(
				$wpdb->prepare(
					"
						UPDATE {$wpdb->pmpro_memberships_users}
						SET status = %s,
							enddate = %s
						WHERE user_id = %d
							AND membership_id = %d
							AND status = %s
							AND id != %d
					",
					'changed',
					current_time( 'mysql' ),
					$user_id,
					$level_id,
					'active',
					$wpdb->insert_id // Ignore the membership that we just added.
				)
			);
		}
	}

	// Clear the level cache for this user.
	pmpro_clear_level_cache_for_user( $user_id );

	/**
	 * Action to run after the membership level changes.
	 *
	 * @param int $level_id ID of the level changed to.
	 * @param int $user_id ID of the user changed.
	 * @param int $cancel_level ID of the level being cancelled if specified.
	 */
	do_action( 'pmpro_after_change_membership_level', $level_id, $user_id, null );

	return true;
}

/**
 * Set old user levels to be used in the pmpro_do_action_after_all_membership_level_changes() function.
 *
 * @param int $user_id ID of the user to set the old levels for.
 */
function pmpro_set_old_user_levels( $user_id ) {
	global $pmpro_old_user_levels;
	if ( empty( $pmpro_old_user_levels ) ) {
		$pmpro_old_user_levels = array();
	}
	if ( ! array_key_exists( $user_id, $pmpro_old_user_levels ) ) {
		$old_levels = pmpro_getMembershipLevelsForUser( $user_id );
		$pmpro_old_user_levels[$user_id] = empty( $old_levels ) ? array() : $old_levels;
	}
}

/**
 * Clear the membership level cache for a user.
 *
 * @param int $user_id ID of the user to clear the cache for.
 */
function pmpro_clear_level_cache_for_user( $user_id ) {
	// Removed cached global.
	global $all_membership_levels;
	unset( $all_membership_levels[ $user_id ] );

	// Remove levels cache for user.
	$cache_key = 'user_' . $user_id . '_levels';
	wp_cache_delete( $cache_key, 'pmpro' );
	wp_cache_delete( $cache_key . '_all', 'pmpro' );
	wp_cache_delete( $cache_key . '_active', 'pmpro' );

	// Update the global user data if cleaning the current user.
	if ( get_current_user_id() === (int) $user_id ) {
		pmpro_set_current_user();
	}
}

/**
 * Runs after all membership level changes have been performed.
 *
 * @param mixed $filter_contents to not break the wp_redirect filter.
 */
function pmpro_do_action_after_all_membership_level_changes( $filter_contents = null ) {
	global $pmpro_old_user_levels;
	if ( empty( $pmpro_old_user_levels ) ) {
		// No level changes occurred, return.
		return $filter_contents;
	}

	// Clear global so that we don't run twice for same level changes
	$pmpro_old_user_levels_copy = $pmpro_old_user_levels;
	$pmpro_old_user_levels = null;

	/**
	 * Run code after all membership level changes have occurred. Users who have had changes
	 * will be stored in the global $pmpro_old_user_levels array.
	 *
	 * @since  2.6
	 * @param array $pmpro_old_user_levels_copy array of user_id => array( old_level_objs )
	 */
	do_action( 'pmpro_after_all_membership_level_changes', $pmpro_old_user_levels_copy );

	return $filter_contents;
}
add_action( 'template_redirect', 'pmpro_do_action_after_all_membership_level_changes', 2 );
add_filter( 'wp_redirect', 'pmpro_do_action_after_all_membership_level_changes', 100 );
add_action( 'pmpro_membership_post_membership_expiry', 'pmpro_do_action_after_all_membership_level_changes' );
add_action( 'shutdown', 'pmpro_do_action_after_all_membership_level_changes' );

/**
 * Function to list WordPress categories in hierarchical format.
 *
 * This is a helper function for the Membership Categories section in adminpages/membershiplevels.php
 *
 * @since 1.8.11
 *
 * @param int   $parent_id
 * @param array $level_categories
 */
function pmpro_listCategories( $parent_id = 0, $level_categories = array() ) {

	$args = array(
		'parent' => $parent_id,
		'hide_empty' => false,
	);

	$cats = get_categories( apply_filters( 'pmpro_list_categories_args', $args ) );

	if ( $cats ) {
		foreach ( $cats as $cat ) {
			if ( ! empty( $level_categories ) ) {
				$checked = checked( in_array( $cat->term_id, $level_categories ), true, false );
			} else {
				$checked = '';
			} ?>
			<div class="pmpro_clickable">
				<input type="checkbox" name="membershipcategory_<?php echo esc_attr( $cat->term_id ); ?>" id="membershipcategory_<?php echo esc_attr( $cat->term_id ); ?>" value="yes" <?php echo esc_attr( $checked ); ?>>
				<label for="membershipcategory_<?php echo esc_attr( $cat->term_id ); ?>"><?php echo esc_html( $cat->name ); ?></label>
			</div>
			<?php pmpro_listCategories( $cat->term_id, $level_categories ); ?>
			<?php
		}
	}
}

/**
 * pmpro_toggleMembershipCategory() creates or deletes a linking entry between the membership level and post category tables.
 *
 * @param int $level may either be the ID or name of the desired membership_level.
 * @param int $category must be a valid post category ID.
 * @param bool $value
 *
 * @return string|true
 * Return values:
 *		Success returns boolean true.
 *		Failure returns a string containing the error message.
 */
function pmpro_toggleMembershipCategory( $level, $category, $value ) {
	global $wpdb;
	$category = intval( $category );

	if ( ( $level = intval( $level ) ) <= 0 ) {
		$safe = addslashes( $level );
		if ( ( $level = intval( $wpdb->get_var( "SELECT id FROM {$wpdb->pmpro_membership_levels} WHERE name = '" . esc_sql( $safe ) . "' LIMIT 1" ) ) ) <= 0 ) {
			return __( 'Membership level not found.', 'paid-memberships-pro' );
		}
	}

	if ( $value ) {
		$sql = "REPLACE INTO {$wpdb->pmpro_memberships_categories} (`membership_id`,`category_id`) VALUES ('" . esc_sql( $level ) . "','" . esc_sql( $category ) . "')";
		$wpdb->query( $sql );
		if ( $wpdb->last_error ) {
			return $wpdb->last_error;
		}
	} else {
		$sql = "DELETE FROM {$wpdb->pmpro_memberships_categories} WHERE `membership_id` = '" . esc_sql( $level ) . "' AND `category_id` = '" . esc_sql( $category ). "' LIMIT 1";
		$wpdb->query( $sql );
		if ( $wpdb->last_error ) {
			return $wpdb->last_error;
		}
	}

	return true;
}

/**
 * pmpro_updateMembershipCategories() ensures that all those and only those categories given
 * are associated with the given membership level.
 *
 * @param string|int $level is a valid membership level ID or name
 * @param int[] $categories is an array of post category IDs
 *
 * @return string|true
 * Return values:
 *		Success returns boolean true.
 *		Failure returns a string containing the error message.
 */
function pmpro_updateMembershipCategories( $level, $categories ) {
	global $wpdb;

	if ( ! is_numeric( $level ) ) {
		$level = $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_membership_levels WHERE name = '" . esc_sql( $level ) . "' LIMIT 1" );
		if ( empty( $level ) ) {
			return __( 'Membership level not found.', 'paid-memberships-pro' );
		}
	}

	// Remove existing category links (and links to deleted terms) only. Restrictions for other taxonomies (e.g. tags) are set on the term edit screen, so leave them in place.
	$sqlQuery = "DELETE mc FROM $wpdb->pmpro_memberships_categories mc
				 LEFT JOIN $wpdb->term_taxonomy tt ON tt.term_id = mc.category_id
				 WHERE mc.membership_id = '" . esc_sql( $level ) . "'
				 AND ( tt.term_taxonomy_id IS NULL OR tt.taxonomy = 'category' )";
	$wpdb->query( $sqlQuery );
	if ( $wpdb->last_error ) {
		return $wpdb->last_error;
	}

	// add the given links [back?] in...
	foreach ( $categories as $cat ) {
		if ( is_string( $r = pmpro_toggleMembershipCategory( $level, $cat, true ) ) ) {
			// uh oh, error
			return $r;
		}
	}

	// all good
	return true;
}

/**
 * pmpro_getMembershipCategories() returns the categories for a given level
 *
 * @param int $level_id The membership level ID.
 * @return array List of category IDs.
 */
function pmpro_getMembershipCategories( $level_id ) {
	static $cache = array();

	$level_id = intval( $level_id );
	if ( isset( $cache[ $level_id ] ) ) {
		return $cache[ $level_id ];
	}

	global $wpdb;
	$categories = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT c.category_id
			 FROM {$wpdb->pmpro_memberships_categories} AS c
			 WHERE c.membership_id = %d",
			$level_id
		)
	);

	$cache[ $level_id ] = $categories;

	return $categories;
}

/**
 * pmpro_isAdmin() checks if a user is an admin.
 *
 * @since 1.8.11
 *
 * @param int|null $user_id The user ID to check. If null, uses the current user.
 *
 * @return bool True if the user is an admin, false otherwise.
 */
function pmpro_isAdmin( $user_id = null ) {
	global $current_user;
	if ( ! $user_id ) {
		$user_id = $current_user->ID;
	}

	if ( ! $user_id ) {
		return false;
	}

	$admincap = user_can( $user_id, 'manage_options' );
	if ( $admincap ) {
		return true;
	} else {
		return false;
	}
}

/**
 * pmpro_replaceUserMeta() updates user meta values, replacing existing values.
 *
 * @since 1.8.11
 *
 * @param int $user_id User ID to update.
 * @param string|array $meta_keys Meta keys to update.
 * @param string|array $meta_values Meta values to set.
 * @param string|array|null $prev_values Previous values to check against.
 *
 * @return int Number of meta keys updated.
 */
function pmpro_replaceUserMeta( $user_id, $meta_keys, $meta_values, $prev_values = null ) {
	// expects all arrays for last 3 params or all strings
	if ( ! is_array( $meta_keys ) ) {
		$meta_keys = array( $meta_keys );
		$meta_values = array( $meta_values );
		$prev_values = array( $prev_values );
	}

	for ( $i = 0; $i < count( $meta_values ); $i++ ) {
		if ( isset( $prev_values[ $i ] ) ) {
			update_user_meta( $user_id, $meta_keys[ $i ], $meta_values[ $i ], $prev_values[ $i ] );
		} else {
			$old_value = get_user_meta( $user_id, $meta_keys[ $i ], true );
			if ( $old_value ) {
				update_user_meta( $user_id, $meta_keys[ $i ], $meta_values[ $i ], $old_value );
			} else {
				update_user_meta( $user_id, $meta_keys[ $i ], $meta_values[ $i ] );
			}
		}
	}

	return $i;
}

/**
 * pmpro_getMetavalues() returns an object with the meta values from a query.
 *
 * @since 1.8.11
 *
 * @param string $query SQL query to get meta values.
 * @return stdClass Object with meta keys and values.
 */
function pmpro_getMetavalues( $query ) {
	global $wpdb;

	$results = $wpdb->get_results( $query );
	$r = new stdClass();
	foreach ( $results as $result ) {
		if ( ! empty( $r ) && ! empty( $result->key ) ) {
			$r->{$result->key} = $result->value;
		}
	}

	return $r;
}

/**
 * Function to return the pagination string
 *
 * @since 1.0.0
 *
 * @param int    $page       Current page number
 * @param int    $totalitems Total number of items
 * @param int    $limit      Items per page
 * @param int    $adjacents  Number of adjacent pages to show
 * @param string $targetpage Target page URL
 * @param string $pagestring Query string parameter for page number
 * @param string $aria_label  aria-label for the pagination
 * @return string HTML for pagination
 */
function pmpro_getPaginationString( $page = 1, $totalitems = 0, $limit = 15, $adjacents = 1, $targetpage = '/', $pagestring = '&pn=', $aria_label = '' ) {

	if ( ! $aria_label ) {
		$aria_label = __( 'Pagination', 'paid-memberships-pro' );
	}

	// other vars
	$prev = $page - 1;                                  // previous page is page - 1
	$next = $page + 1;                                  // next page is page + 1
	$lastpage = ceil( $totalitems / $limit );             // lastpage is = total items / items per page, rounded up.
	$lpm1 = $lastpage - 1;                              // last page minus 1

	$pagination = '';
	if ( $lastpage > 1 ) {
		$pagination .= '<nav class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination' ) ) . '" aria-label="' . esc_attr( $aria_label ) . '">';

		// previous button
		if ( $page > 1 ) {
			$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-previous' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $prev ) . '" ';
			$pagination .= 'aria-label="' . esc_attr__( 'Go to the previous page', 'paid-memberships-pro' ) . '">&laquo; ' . esc_html__( 'Previous', 'paid-memberships-pro' ) . '</a>';
		} else {
			$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-previous pmpro_pagination-disabled' ) ) . '">&laquo; ' . esc_html__( 'Previous', 'paid-memberships-pro' ) . '</span>';
		}

		// pages
		if ( $lastpage < 7 + ( $adjacents * 2 ) ) {
			for ( $counter = 1; $counter <= $lastpage; $counter++ ) {
				if ( $counter == $page ) {
					$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page pmpro_pagination-current' ) ) . '" aria-current="page">' . esc_html( $counter ) . '</span>';
				} else {
					$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $counter ) . '" ';
					/* translators: %s: page number */
					$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $counter ) ) . '">' . esc_html( $counter ) . '</a>';
				}
			}
		} elseif ( $lastpage >= 7 + ( $adjacents * 2 ) ) {
			// close to beginning; only hide later pages
			if ( $page < 1 + ( $adjacents * 3 ) ) {
				for ( $counter = 1; $counter < 4 + ( $adjacents * 2 ); $counter++ ) {
					if ( $counter == $page ) {
						$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page pmpro_pagination-current' ) ) . '" aria-current="page">' . esc_html( $counter ) . '</span>';
					} else {
						$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $counter ) . '" ';
						/* translators: %s: page number */
						$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $counter ) ) . '">' . esc_html( $counter ) . '</a>';
					}
				}
				$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-dots' ) ) . '" aria-hidden="true">&hellip;</span>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $lpm1 ) . '" ';
				/* translators: %s: page number */
				$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $lpm1 ) ) . '">' . esc_html( $lpm1 ) . '</a>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $lastpage ) . '" ';
				/* translators: %s: page number */
				$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $lastpage ) ) . '">' . esc_html( $lastpage ) . '</a>';
			} // in middle; hide some front and some back
			elseif ( $lastpage - ( $adjacents * 2 ) > $page && $page > ( $adjacents * 2 ) ) {
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . '1' ) . '" ';
				$pagination .= 'aria-label="' . esc_attr__( 'Page 1', 'paid-memberships-pro' ) . '">1</a>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . '2' ) . '" ';
				$pagination .= 'aria-label="' . esc_attr__( 'Page 2', 'paid-memberships-pro' ) . '">2</a>';
				$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-dots' ) ) . '" aria-hidden="true">&hellip;</span>';
				for ( $counter = $page - $adjacents; $counter <= $page + $adjacents; $counter++ ) {
					if ( $counter == $page ) {
						$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page pmpro_pagination-current' ) ) . '" aria-current="page">' . esc_html( $counter ) . '</span>';
					} else {
						$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $counter ) . '" ';
						/* translators: %s: page number */
						$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $counter ) ) . '">' . esc_html( $counter ) . '</a>';
					}
				}
				$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-dots' ) ) . '" aria-hidden="true">&hellip;</span>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $lpm1 ) . '" ';
				/* translators: %s: page number */
				$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $lpm1 ) ) . '">' . esc_html( $lpm1 ) . '</a>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $lastpage ) . '" ';
				/* translators: %s: page number */
				$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $lastpage ) ) . '">' . esc_html( $lastpage ) . '</a>';
			} // close to end; only hide early pages
			else {
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . '1' ) . '" ';
				$pagination .= 'aria-label="' . esc_attr__( 'Page 1', 'paid-memberships-pro' ) . '">1</a>';
				$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . '2' ) . '" ';
				$pagination .= 'aria-label="' . esc_attr__( 'Page 2', 'paid-memberships-pro' ) . '">2</a>';
				$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-dots' ) ) . '" aria-hidden="true">&hellip;</span>';
				for ( $counter = $lastpage - ( 1 + ( $adjacents * 3 ) ); $counter <= $lastpage; $counter++ ) {
					if ( $counter == $page ) {
						$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page pmpro_pagination-current' ) ) . '" aria-current="page">' . esc_html( $counter ) . '</span>';
					} else {
						$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-page' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $counter ) . '" ';
						/* translators: %s: page number */
						$pagination .= 'aria-label="' . esc_attr( sprintf( __( 'Page %s', 'paid-memberships-pro' ), $counter ) ) . '">' . esc_html( $counter ) . '</a>';
					}
				}
			}
		}

		// next button
		if ( $page < $counter - 1 ) {
			$pagination .= '<a class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-next' ) ) . '" href="' . esc_url( $targetpage . $pagestring . $next ) . '" ';
			$pagination .= 'aria-label="' . esc_attr__( 'Go to the next page', 'paid-memberships-pro' ) . '">' . esc_html__( 'Next', 'paid-memberships-pro' ) . ' &raquo;</a>';
		} else {
			$pagination .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_pagination-next pmpro_pagination-disabled' ) ) . '">' . esc_html__( 'Next', 'paid-memberships-pro' ) . ' &raquo;</span>';
		}
		$pagination .= "</nav>\n";
	}

	/**
	 * Filters the pagination HTML
	 *
	 * @since 2.0.0
	 *
	 * @param string $pagination The pagination HTML
	 * @param int    $page       Current page number
	 * @param int    $totalitems Total number of items
	 * @param int    $limit      Items per page
	 * @param int    $adjacents  Number of adjacent pages to show
	 * @param string $targetpage Target page URL
	 * @param string $pagestring Query string parameter for page number
	 * @return string The filtered pagination HTML
	 */
	return apply_filters( 'pmpro_get_pagination_string', $pagination, $page, $totalitems, $limit, $adjacents, $targetpage, $pagestring, $aria_label );
}

function pmpro_calculateInitialPaymentRevenue( $s = null, $l = null ) {
	global $wpdb;

	// if we're limiting users by search
	if ( $s || $l ) {
		$user_ids_query = "SELECT u.ID FROM $wpdb->users u LEFT JOIN $wpdb->usermeta um  ON u.ID = um.user_id LEFT JOIN $wpdb->pmpro_memberships_users mu ON u.ID = mu.user_id WHERE mu.status = 'active' ";
		if ( $s ) {
			$user_ids_query .= "AND (u.user_login LIKE '%" . esc_sql( $s ) . "%' OR u.user_email LIKE '%" . esc_sql( $s ) . "%' OR um.meta_value LIKE '%$" . esc_sql( $s ) . "%') ";
		}
		if ( $l ) {
			$user_ids_query .= "AND mu.membership_id = '" . esc_sql( $l ) . "' ";
		}
	}

	// query to sum initial payments
	$sqlQuery = "SELECT SUM(initial_payment) FROM $wpdb->pmpro_memberships_users WHERE `status` = 'active' ";
	if ( ! empty( $user_ids_query ) ) {
		$sqlQuery .= 'AND user_id IN(' . $user_ids_query . ') ';
	}

	$total = $wpdb->get_var( $sqlQuery );

	return (float) $total;
}

function pmpro_calculateRecurringRevenue( $s, $l ) {
	global $wpdb;

	// if we're limiting users by search
	if ( $s || $l ) {
		$user_ids_query = "AND user_id IN(SELECT u.ID FROM $wpdb->users u LEFT JOIN $wpdb->usermeta um  ON u.ID = um.user_id LEFT JOIN $wpdb->pmpro_memberships_users mu ON u.ID = mu.user_id WHERE mu.status = 'active' ";
		if ( $s ) {
			$user_ids_query .= "AND (u.user_login LIKE '%" . esc_sql( $s ) . "%' OR u.user_email LIKE '%" . esc_sql( $s ) . "%' OR um.meta_value LIKE '%" . esc_sql( $s ) . "%') ";
		}
		if ( $l ) {
			$user_ids_query .= "AND mu.membership_id = '" . esc_sql( $l ) . "' ";
		}
		$user_ids_query .= ")";
	} else {
		$user_ids_query = '';
	}

	// 4 queries to get annual earnings for each cycle period. currently ignoring trial periods and billing limits.
	$sqlQuery = "
		SELECT SUM((12/cycle_number)*billing_amount) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND cycle_period = 'Month' AND cycle_number <> 12 $user_ids_query
			UNION
		SELECT SUM((365/cycle_number)*billing_amount) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND cycle_period = 'Day' AND cycle_number <> 365 $user_ids_query
			UNION
		SELECT SUM((24/cycle_number)*billing_amount) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND cycle_period = 'Hour' AND cycle_number <> 24 $user_ids_query
			UNION
		SELECT SUM((52/cycle_number)*billing_amount) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND cycle_period = 'Week' AND cycle_number <> 52 $user_ids_query
			UNION
		SELECT SUM(billing_amount) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND cycle_period = 'Year' $user_ids_query
	";

	$annual_revenues = $wpdb->get_col( $sqlQuery );

	$total = 0;
	foreach ( $annual_revenues as $r ) {
		$total += $r;
	}

	return $total;
}

/**
 * Generate a Username from the provided first name, last name or email address.
 *
 * @param string $firstname User-submitted First Name.
 * @param string $lastname User-submitted Last Name.
 * @param string $email User-submitted Email Address.
 *
 * @return string $username.
 */
function pmpro_generateUsername( $firstname = '', $lastname = '', $email = '' ) {
	// Strip all non-alpha characters from first and last name.
	if ( ! empty( $firstname) ) {
		$firstname = preg_replace( '/[^A-Za-z]/', '', $firstname );
	}
	if ( ! empty( $lastname ) ) {
		$lastname = preg_replace( '/[^A-Za-z]/', '', $lastname );
	}

	// Try to create username using first and last name.
	if ( ! empty( $firstname ) && ! empty( $lastname ) ) {
		// Create username using first initial + last name.
		$username = substr( $firstname, 0, 1 ) . $lastname;
	} elseif ( ! empty( $firstname ) ) {
		// Create username using only first name.
		$username = $firstname;
	} elseif ( ! empty( $lastname ) ) {
		// Create username using only last name.
		$username = $lastname;
	}

	// If no username yet or one based on name exists,
	// try to create username using email address.
	if ( ( empty( $username ) || username_exists( $username ) )
		&& ! empty( $email ) && is_email( $email ) ) {
		// Break email into two parts, before and after the @ symbol.
		$emailparts = explode( '@', $email );
		if ( ! empty( $emailparts ) ) {
			// Set username to the string before the email's @ symbol.
			$email = preg_replace( '/[^A-Za-z0-9]/', '', $emailparts[0] );
			$username = $email;
		}
	}

	// No Username yet. Generate a random one.
	if ( empty( $username ) ) {
		$username = wp_generate_password( 10, false );
	}

	// Check if username is taken and continue to append an incremented number until it is unique.
	$taken = true;
	$count = 0;
	while ( $taken ) {
		// Append a number to the end of the username.
		if ( $count ) {
			$username = preg_replace( '/[0-9]/', '', $username ) . $count;
		}

		// Check if the username is taken.
		$taken = username_exists( $username );

		// Increment the number.
		$count++;
	}

	// Sanitize the username.
	$username = sanitize_user( $username );

	// We must have a good username now.
	return $username;
}

/**
 * Get a new random code for discount codes.
 */
function pmpro_getDiscountCode( $seed = null ) {
	global $wpdb;

	// We mix this with the seed to make sure we get unique codes.
	static $count = 0;
	$count++;

	if( defined( 'AUTH_KEY' ) && defined( 'SECURE_AUTH_KEY' ) ) {
		$auth_code = AUTH_KEY;
		$secure_auth_code = SECURE_AUTH_KEY;
	} else {
		//Generate our own random string and hash it
		$auth_code = md5( rand() );
		$secure_auth_code = md5( rand() );
	}

	while ( empty( $code ) ) {
		$scramble = md5( $auth_code . microtime() . $seed . $secure_auth_code . $count );
		$code = substr( $scramble, 0, 10 );
		$check = $wpdb->get_var( "SELECT code FROM $wpdb->pmpro_discount_codes WHERE code = '" . esc_sql( $code ) . "' LIMIT 1" );
		if ( $check || is_numeric( $code ) ) {
			$code = null;
		}
	}

	return strtoupper( $code );
}

/**
 * Is a discount code valid - $level_id could be a scalar or an array (or unset)
 */
function pmpro_checkDiscountCode( $code, $level_id = null, $return_errors = false ) {
	global $wpdb, $current_user;

	$error = false;
	$dbcode = false;

	// no code, no code
	if ( empty( $code ) ) {
		$error = __( 'No code was given to check.', 'paid-memberships-pro' );
	}

	// get code from db
	if ( ! $error ) {
		$dbcode = $wpdb->get_row( "SELECT *, UNIX_TIMESTAMP(CONVERT_TZ(starts, '+00:00', @@global.time_zone)) as starts, UNIX_TIMESTAMP(CONVERT_TZ(expires, '+00:00', @@global.time_zone)) as expires FROM $wpdb->pmpro_discount_codes WHERE code ='" . esc_sql( $code ) . "' LIMIT 1" );

		// did we find it?
		if ( empty( $dbcode->id ) ) {
			$error = __( 'The discount code could not be found.', 'paid-memberships-pro' );
		}
	}

	// check if the code has started
	if ( ! $error ) {
		// fix the date timestamps
		$dbcode->starts = strtotime( date_i18n( 'm/d/Y 00:00:00', $dbcode->starts ) );
		$dbcode->expires = strtotime( date_i18n( 'm/d/Y 23:59:59', $dbcode->expires ) );

		// today
		$today = strtotime( date_i18n( 'm/d/Y H:i:00', current_time( 'timestamp' ) ) );

		// has this code started yet?
		if ( ! empty( $dbcode->starts ) && $dbcode->starts > $today ) {
			/* translators: %s: start date */
			$error = sprintf( __( 'This discount code goes into effect on %s.', 'paid-memberships-pro' ), date_i18n( get_option( 'date_format' ), $dbcode->starts ) );
		}
	}

	// check if the code is expired
	if ( ! $error ) {
		if ( ! empty( $dbcode->expires ) && $dbcode->expires < $today ) {
			/* translators: %s: end date */
			$error = sprintf( __( 'This discount code expired on %s.', 'paid-memberships-pro' ), date_i18n( get_option( 'date_format' ), $dbcode->expires ) );
		}
	}

	// have we run out of uses?
	if ( ! $error ) {
		if ( $dbcode->uses > 0 ) {
			$used = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->pmpro_discount_codes_uses WHERE code_id = '" . esc_sql( $dbcode->id ) . "'" );
			if ( $used >= $dbcode->uses ) {
				$error = __( 'This discount code is no longer valid.', 'paid-memberships-pro' );
			}
		}
	}

	// check if this code is limited to one use per user
	if ( ! $error ) {
		if ( ! empty( $dbcode->one_use_per_user ) && ! empty( $current_user->ID ) ) {
			$used = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->pmpro_discount_codes_uses WHERE code_id = '" . esc_sql( $dbcode->id ) . "' AND user_id = '" . esc_sql( $current_user->ID ) . "'" );
			if ( $used > 0 ) {
				$error = __( 'You have already used the discount code provided.', 'paid-memberships-pro' );
			}
		}
	}

	// if a level was passed check if this code applies
	if ( ! $error ) {
		$pmpro_check_discount_code_levels = apply_filters( 'pmpro_check_discount_code_levels', true, $dbcode->id );
		if ( ! empty( $level_id ) && $pmpro_check_discount_code_levels ) {
			// clean up level id for security before the database call
			if ( is_array( $level_id ) ) {
				$levelnums = array_map( 'intval', $level_id );
				$level_id = implode( ',', $levelnums );
			} else {
				$level_id = intval( $level_id );
			}
			$code_level = $wpdb->get_row( "SELECT l.id, cl.*, l.name, l.description, l.allow_signups FROM $wpdb->pmpro_discount_codes_levels cl LEFT JOIN $wpdb->pmpro_membership_levels l ON cl.level_id = l.id WHERE cl.code_id = '" . esc_sql( $dbcode->id ) . "' AND cl.level_id IN (" . $level_id . ") LIMIT 1" ); // $level_id is already escaped above.

			if ( empty( $code_level ) ) {
				$error = __( 'This discount code does not apply to this membership level.', 'paid-memberships-pro' );
			}
		}
	}

	/**
	 * Filter the results of the discount code check.
	 *
	 * @since 1.7.13.1
	 *
	 * @param bool $okay true if code check is okay or false if there was an error
	 * @param object $dbcode Object containing code data from the database row
	 * @param int|array $level_id ID of the level the user is checking out for.
	 * @param string $code Discount code string.
	 *
	 * @return mixed $okay true if okay, false or error message string if not okay
	 */
	$okay = ! $error;
	$pmpro_check_discount_code = apply_filters( 'pmpro_check_discount_code', $okay, $dbcode, $level_id, $code );
	if ( is_string( $pmpro_check_discount_code ) ) {
		$error = $pmpro_check_discount_code;    // string returned, this is an error
	} elseif ( ! $pmpro_check_discount_code && ! $error ) {
		$error = true;                          // no error before, but filter returned error
	} elseif ( $pmpro_check_discount_code ) {
		$error = false;                         // filter is true, so error false
	}

	// return
	if ( $error ) {
		// there was an error
		if ( ! empty( $return_errors ) ) {
			return array( false, $error );
		} else {
			return false;
		}
	} else {
		// guess we're all good
		if ( ! empty( $return_errors ) ) {
			return array( true, __( 'This discount code is okay.', 'paid-memberships-pro' ) );
		} else {
			return true;
		}
	}
}

function pmpro_no_quotes( $s, $quotes = array( "'", '"' ) ) {
	if ( empty( $s ) ) {
		$s = '';
	}

	return str_replace( $quotes, '', $s );
}

/**
 * From: http://www.php.net/manual/en/function.implode.php#86845
 */
function pmpro_implodeToEnglish( $array, $conjunction = 'and' ) {
	// sanity check
	if ( ! $array || ! count( $array ) ) {
		return '';
	}

	// get last element
	$last = array_pop( $array );

	// if it was the only element - return it
	if ( ! count( $array ) ) {
		return $last;
	}

	// possibly translate the conjunction
	if ( $conjunction == 'and' ) {
		$conjunction = __( 'and', 'paid-memberships-pro' );
	}

	// List the elements in a comma-separated list.
	$start = implode( ', ', $array );

	// Add the Oxford comma if needed.
	if ( count( $array ) > 1 ) {
		$start .= ',';
	}

	// Output the elements.
	return $start . ' ' . $conjunction . ' ' . $last;
}

// from yoast wordpress seo
function pmpro_text_limit( $text, $limit, $finish = '&hellip;' ) {
	if ( strlen( $text ) > $limit ) {
		$text = substr( $text, 0, $limit );
		$text = substr( $text, 0, - ( strlen( strrchr( $text, ' ' ) ) ) );
		$text .= $finish;
	}
	return $text;
}

/**
 * Filters the separator used between action navigation links.
 *
 * @since 2.3
 */
function pmpro_actions_nav_separator() {
	$separator = apply_filters( 'pmpro_actions_nav_separator', ' | ' );

	return $separator;
}

/**
 * pmpro_get_no_access_message to return the appropriate content message for the protected content.
 *
 * @param string $content The content being shown before the no access message. Usually an excerpt.
 * @param array $level_ids The array of level IDs this post is protected for.
 * @param array $level_names The array of names for the levels this post is protected for.
 *
 * @return string $content The appropriate content message for the given user/visitor and required levels.
 *
 */
function pmpro_get_no_access_message( $content, $level_ids, $level_names = NULL ) {
	if ( empty( $level_ids ) ) {
		$level_ids = array();
	}

	if ( empty( $level_names ) ) {
		$level_names = array();
		foreach ( $level_ids as $key => $id ) {
			$level_obj = pmpro_getLevel( $id );
			if ( ! empty( $level_obj ) ) {
				$level_names[] = $level_obj->name;
			}
		}
	}

	// Hide levels which don't allow signups by default.
	if( ! apply_filters( 'pmpro_membership_content_filter_disallowed_levels', false, $level_ids, $level_names ) ) {
		foreach ( $level_ids as $key => $id ) {
			// Does this level allow registrations?
			$level_obj = pmpro_getLevel( $id );
			if ( empty( $level_obj ) || empty( $level_obj->allow_signups ) ) {
				unset( $level_ids[$key] );
				unset( $level_names[$key] );
			}
		}
	}

	$pmpro_content_mesage_pre = '<div class="' . pmpro_get_element_class( 'pmpro' ) . '"><div class="' . pmpro_get_element_class( 'pmpro_card pmpro_content_message', 'pmpro_content_message' ) . '">';
	$pmpro_content_message_post = '</div></div>';

	$sr_search = array( '!!levels!!', '!!referrer!!', '!!login_url!!', '!!login_page_url!!', '!!levels_url!!', '!!levels_page_url!!' );
	$sr_replace = array( pmpro_implodeToEnglish( $level_names ), urlencode( site_url( esc_url_raw( $_SERVER['REQUEST_URI'] ) ) ), esc_url( pmpro_login_url() ), esc_url( pmpro_login_url() ), esc_url( pmpro_url( 'levels' ) ), esc_url( pmpro_url( 'levels' ) ) );

	// Get the correct message to show at the bottom.
	if ( is_feed() ) {
		$rsstext = __( 'This content is for members only. Visit the site and log in/register to read.', 'paid-memberships-pro' );
		/**
		 * Filter the RSS text for protected content.
		 *
		 * @param string $rsstext The RSS text for protected content.
		 *
		 * @return string $rsstext The filtered RSS text for protected content.
		 */
		$content = apply_filters( 'pmpro_rss_text_filter', $rsstext );
	} else {
		// Not a member. Show our default message or the site's custom message.
		$nonmembertext = get_option( 'pmpro_nonmembertext' );
		if ( ! empty( $nonmembertext ) ) {
			$no_access_message_inner = '<div class="' . pmpro_get_element_class( 'pmpro_card_content' ) . '">';
			$no_access_message_inner .= stripslashes( $nonmembertext );
			$no_access_message_inner .= '</div>';
		} else {
			// Use our generated smart default message.
			// Build the dynamic message contents.
			if ( count( $level_ids ) !== 1 ) {
				$header = __( 'Membership Required', 'paid-memberships-pro' );
				$body = '<p>' . __(' You must be a member to access this content.', 'paid-memberships-pro') . '</p>';
				// Add a link to the levels page if it's set.
				if ( ! empty( pmpro_url( 'levels' ) ) ) {
					$body .= '<p><a class="' . pmpro_get_element_class( 'pmpro_btn' ) . '" href="!!levels_page_url!!">' . __( 'View Membership Levels', 'paid-memberships-pro' ) . '</a></p>';
				}
			} else {
				$header = __( '!!levels!! Membership Required', 'paid-memberships-pro' );
				$body = '<p>' . __(' You must be a !!levels!! member to access this content.', 'paid-memberships-pro') . '</p>';
				if ( ! empty( pmpro_url( 'checkout' ) ) ) {
					$body .= '<p><a class="' . pmpro_get_element_class( 'pmpro_btn' ) . '" href="' . esc_url( pmpro_url( 'checkout', '?pmpro_level=' . current( $level_ids ) ) ) . '">' . __( 'Join Now', 'paid-memberships-pro' ) . '</a></p>';
				}
			}
			/**
			 * Filter the header message for the no access message.
			 *
			 * @since 3.1
			 *
			 * @param string $header The header message for the no access message.
			 * @param array $level_ids The array of level IDs this post is protected for.
			 */
			$header = apply_filters( 'pmpro_no_access_message_header', $header, $level_ids );

			/**
			 * Filter the body message for the no access message.
			 *
			 * @since 3.1
			 *
			 * @param string $body The body message for the no access message.
			 * @param array $level_ids The array of level IDs this post is protected for.
			 */
			$body = apply_filters( 'pmpro_no_access_message_body', $body, $level_ids );

			/**
			 * Legacy filter for logged-out message for non-members/logged-out visitors.
			 *
			 * @deprecated 3.1
			 */
			if ( ! is_user_logged_in() ) {
				$body = apply_filters_deprecated( 'pmpro_not_logged_in_text_filter', array( $body ), '3.1', 'pmpro_no_access_message_body' );
			} else {
				$body = apply_filters_deprecated( 'pmpro_non_member_text_filter', array( $body ), '3.1', 'pmpro_no_access_message_body' );
			}

			// Build the content message.
			$no_access_message_inner = '<h2 class="' . pmpro_get_element_class( 'pmpro_card_title pmpro_font-large' ) . '">';
			$no_access_message_inner .= '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--pmpro--color--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-lock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>';
			$no_access_message_inner .= $header;
			$no_access_message_inner .= '</h2>';
			$no_access_message_inner .= '<div class="' . pmpro_get_element_class( 'pmpro_card_content' ) . '">' . $body . '</div>';
		}

		// If the user is not logged in, show a link to log in if the message doesn't already have one.
		if ( ! is_user_logged_in() && strpos( $no_access_message_inner, '!!login' ) === false ) {
			$no_access_message_inner .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_card_actions pmpro_font-medium' ) ) . '">';
			// to do redirect back to content.
			$no_access_message_inner .= esc_html__( 'Already a member?', 'paid-memberships-pro' ) . ' <a href="' . esc_url( wp_login_url( get_permalink() ) ) . '">' . esc_html__( 'Log in here', 'paid-memberships-pro' ) . '</a>';
			$no_access_message_inner .= '</div>';
		}

		// Add the pre and post content message.
		$no_access_message_html = $pmpro_content_mesage_pre . str_replace( $sr_search, $sr_replace, $no_access_message_inner ) . $pmpro_content_message_post;
		/**
		 * Filter the HTML of the no access message.
		 *
		 * @since 3.1
		 *
		 * @param string $content_message The HTML of the no access message.
		 * @param array $level_ids The array of level IDs this post is protected for.
		 */
		$no_access_message_html = apply_filters( 'pmpro_no_access_message_html', $no_access_message_html, $level_ids );

		$content .= $no_access_message_html;
	}

	return $content;
}

/**
 * pmpro_getMembershipLevelForUser() returns the first active membership level for a user
 *
 * If $user_id is omitted, the value will be retrieved from $current_user.
 *
 * @return false|object
 * Return values:
 *		Success returns the level object.
 *		Failure returns false.
 */
function pmpro_getMembershipLevelForUser( $user_id = null, $force = false ) {
	if ( empty( $user_id ) ) {
		global $current_user;
		$user_id = $current_user->ID;
	}

	if ( empty( $user_id ) ) {
		return false;
	}

	// make sure user id is int for security
	$user_id = intval( $user_id );

	global $all_membership_levels;

	if ( isset( $all_membership_levels[ $user_id ] ) && ! $force ) {
		return $all_membership_levels[ $user_id ];
	} else {
		global $wpdb;
		$all_membership_levels[ $user_id ] = $wpdb->get_row(
			"SELECT
				l.id AS ID,
				l.id as id,
				mu.id as subscription_id,
				l.name AS name,
				l.description,
				l.confirmation,
				l.expiration_number,
				l.expiration_period,
				l.allow_signups,
				mu.initial_payment,
				mu.billing_amount,
				mu.cycle_number,
				mu.cycle_period,
				mu.billing_limit,
				mu.trial_amount,
				mu.trial_limit,
				mu.code_id as code_id,
				UNIX_TIMESTAMP( CONVERT_TZ(startdate, '+00:00', @@global.time_zone) ) as startdate,
				UNIX_TIMESTAMP( CONVERT_TZ(enddate, '+00:00', @@global.time_zone) ) as enddate
			FROM {$wpdb->pmpro_membership_levels} AS l
			JOIN {$wpdb->pmpro_memberships_users} AS mu ON (l.id = mu.membership_id)
			WHERE mu.user_id = $user_id AND mu.status = 'active'
			LIMIT 1"
		);

		// if null, change to false to avoid user meta conflicts
		if ( empty( $all_membership_levels[ $user_id ] ) ) {
			$all_membership_levels[ $user_id ] = false;
		}

		// Round off prices
		if ( ! empty( $all_membership_levels[$user_id] ) ) {
			if ( isset( $all_membership_levels[$user_id]->initial_payment ) ) {
				$all_membership_levels[$user_id]->initial_payment = pmpro_round_price( $all_membership_levels[$user_id]->initial_payment );
			}
			if ( isset( $all_membership_levels[$user_id]->billing_amount ) ) {
				$all_membership_levels[$user_id]->billing_amount = pmpro_round_price( $all_membership_levels[$user_id]->billing_amount );
			}
			if ( isset( $all_membership_levels[$user_id]->trial_amount ) ) {
				$all_membership_levels[$user_id]->trial_amount = pmpro_round_price( $all_membership_levels[$user_id]->trial_amount );
			}
		}

		/**
		 * pmpro_get_membership_level_for_user filter.
		 *
		 * Filters the returned level.
		 *
		 * @since 1.8.5.4
		 *
		 * @param object $level Level object.
		 */
		$all_membership_levels[ $user_id ] = apply_filters( 'pmpro_get_membership_level_for_user', $all_membership_levels[ $user_id ], $user_id );

		return $all_membership_levels[ $user_id ];
	}
}

/**
 * pmpro_getMembershipLevelsForUser() returns the membership levels for a user
 *
 * If $user_id is omitted, the value will be retrieved from $current_user.
 * By default it only includes active memberships.
 *
 * @return array|false
 * Return values:
 *		Success returns an array of level objects.
 *		Failure returns false.
 */
function pmpro_getMembershipLevelsForUser( $user_id = null, $include_inactive = false ) {
	global $current_user, $pmpro_pages;
	if ( empty( $user_id ) ) {
		$user_id = $current_user->ID;
	}

	if ( empty( $user_id ) ) {
		return false;
	}

	// make sure user id is int for security
	$user_id = intval( $user_id );

	// Admins have special rules for membership levels. Check them here.
	if ( $user_id == $current_user->ID && current_user_can( 'manage_options' ) ) {
		// Make sure that we are not on a page where we want to always show the user's true levels.
		if (
			! is_admin() &&
			( empty( $GLOBALS['wp_query'] ) || ! pmpro_is_checkout() ) &&
			( empty( $pmpro_pages['account'] ) || ! is_page( $pmpro_pages['account'] ) ) &&
			( empty( $pmpro_pages['billing'] ) || ! is_page( $pmpro_pages['billing'] ) ) &&
			( empty( $pmpro_pages['cancel'] ) || ! is_page( $pmpro_pages['cancel'] ) ) &&
			( empty( $pmpro_pages['checkout'] ) || ! is_page( $pmpro_pages['checkout'] ) ) &&
			( empty( $pmpro_pages['confirmation'] ) || ! is_page( $pmpro_pages['confirmation'] ) ) &&
			( empty( $pmpro_pages['invoice'] ) || ! is_page( $pmpro_pages['invoice'] ) ) &&
			( empty( $pmpro_pages['levels'] ) || ! is_page( $pmpro_pages['levels'] ) ) &&
			! apply_filters( 'pmpro_disable_admin_membership_access', false )
		) {
			// This user meta can be changed via the admin bar.
			$admin_membership_access = get_user_meta( $current_user->ID, 'pmpro_admin_membership_access', true );

			if ( 'no' === $admin_membership_access ) {
				return array();
			} elseif ( 'yes' === $admin_membership_access ) {
				$all_levels = pmpro_getAllLevels( true );

				// Make sure that each level has all the necessary fields.
				foreach ( $all_levels as $key => $level ) {
					$level->ID = $level->id;
					$level->subscription_id = null;
					$level->code_id = null;
					$level->startdate = strtotime( '-1 year' );
					$level->enddate = strtotime ( '+1 year' );
				}
				return $all_levels;
			}

			// If we get here, the admin membership access is set to 'current'.
			// Continue checking access as normal.
		}
	}

	global $wpdb;

	/**
	 * We are going to see if cache is set before doing the query and use that if it is.
	 *
	 * In a default environment with no external object cache, the value is cached in that request and
	 * reduces future MySQL requests. If there is an external object cache like Redis then it will be
	 * persisted until the user level changes.
	 **/
	$cache_key = 'user_' . $user_id . '_levels' . ( $include_inactive ? '_all' : '_active' );
    $levels = wp_cache_get( $cache_key, 'pmpro' );

	if ( $levels === false ) {

		$levels = $wpdb->get_results(
			"SELECT
				l.id AS ID,
				l.id as id,
				mu.id as subscription_id,
				l.name,
				l.description,
				l.confirmation,
				l.expiration_number,
				l.expiration_period,
				l.allow_signups,
				mu.initial_payment,
				mu.billing_amount,
				mu.cycle_number,
				mu.cycle_period,
				mu.billing_limit,
				mu.trial_amount,
				mu.trial_limit,
				mu.status,
				mu.code_id as code_id,
				UNIX_TIMESTAMP(CONVERT_TZ(startdate, '+00:00', @@global.time_zone)) as startdate,
				UNIX_TIMESTAMP(CONVERT_TZ(enddate, '+00:00', @@global.time_zone)) as enddate
			FROM {$wpdb->pmpro_membership_levels} AS l
			JOIN {$wpdb->pmpro_memberships_users} AS mu ON (l.id = mu.membership_id)
			WHERE mu.user_id = $user_id" . ( $include_inactive ? '' : " AND mu.status = 'active'
			GROUP BY ID" )
		);
		wp_cache_set( $cache_key, $levels, 'pmpro', 3600 );
	}

	// Round off prices
	if ( ! empty( $levels ) ) {
		foreach( $levels as $key => $level ) {
			$levels[$key]->initial_payment = pmpro_round_price( $level->initial_payment );
			$levels[$key]->billing_amount = pmpro_round_price( $level->billing_amount );
			$levels[$key]->trial_amount = pmpro_round_price( $level->trial_amount );
		}
	}

	/**
	 * pmpro_get_membership_levels_for_user filter.
	 *
	 * Filters the returned levels.
	 *
	 * @since 1.8.5.4
	 *
	 * @param array $levels Array of level objects.
	 */
	$levels = apply_filters( 'pmpro_get_membership_levels_for_user', $levels, $user_id );

	return $levels;
}

/**
 * Get a specific membership level for a user if they have that level.
 * This is better to use when MMPU is enabled on the site.
 *
 * If $user_id is null, the value will be retrieved from $current_user.
 *
 * @param int $user_id User ID to check for
 * @param int $level_id Level ID to check for.
 *
 * @return false|object
 * Return values:
 *      Success returns the level object.
 *      Failure returns false.
 */
function pmpro_getSpecificMembershipLevelForUser( $user_id, $level_id ) {
	if ( empty( $user_id ) ) {
		global $current_user;
		$user_id = $current_user->ID;
	}

	if ( empty( $user_id ) || empty( $level_id ) ) {
		return false;
	}

	$all_levels = pmpro_getMembershipLevelsForUser( $user_id );

	foreach ( $all_levels as $level ) {
		if ( $level->id == $level_id ) {
			return $level;
		}
	}

	return false;
}

/**
 * pmpro_getLevel() returns the level object for a level
 *
 * @param int|string|object $level may be the level id or name
 *
 * @return false|object
 * Return values:
 *		Success returns the level object.
 *		Failure returns false.
 */
function pmpro_getLevel( $level ) {
	global $pmpro_levels;

	if ( is_object( $level ) && ! empty( $level->id ) ) {
		$level = $level->id;
	}

	// was a name passed? (Todo: make sure level names have at least one non-numeric character.
	if ( is_numeric( $level ) ) {
		$level_id = intval( $level );
		if ( isset( $pmpro_levels[ $level_id ] ) ) {
			return $pmpro_levels[ $level_id ];
		} else {
			global $wpdb;
			$pmpro_levels[ $level_id ] = $wpdb->get_row( "SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . esc_sql( $level_id ) . "' LIMIT 1" );
		}
	} else {
		global $wpdb;
		$level_obj = $wpdb->get_row( "SELECT * FROM $wpdb->pmpro_membership_levels WHERE name = '" . esc_sql( $level ) . "' LIMIT 1" );

		if ( ! empty( $level_obj ) ) {
			$level_id = $level_obj->id;
		} else {
			return false;
		}

		$pmpro_levels[ $level_id ] = $level_obj;
	}

	// Round prices
	if ( ! empty( $pmpro_levels[ $level_id ] ) ) {
		$pmpro_levels[ $level_id ]->initial_payment = pmpro_round_price( $pmpro_levels[ $level_id ]->initial_payment );
		$pmpro_levels[ $level_id ]->billing_amount = pmpro_round_price( $pmpro_levels[ $level_id ]->billing_amount );
		$pmpro_levels[ $level_id ]->trial_amount = pmpro_round_price( $pmpro_levels[ $level_id ]->trial_amount );
	}

	return $pmpro_levels[ $level_id ];
}

/**
 * Get all PMPro membership levels.
 *
 * @since 3.0 deprecated the second `$use_cache` argument.
 *
 * @param bool $include_hidden      Include levels marked as hidden/inactive.
 * @param bool $deprecated_argument No longer used.
 * @param bool $force               Resets the static var caches.
 */
function pmpro_getAllLevels( $include_hidden = false, $deprecated_argument = false, $force = false ) {
	// The global $pmpro_levels variable is deprecated and should no longer be used, but we'll set it for backwards compatibility.
	global $pmpro_levels, $wpdb;

	static $pmpro_all_levels;			// every single level
	static $pmpro_visible_levels;		// every single level that's not hidden

	if ( $force ) {
		$pmpro_levels = NULL;
		$pmpro_all_levels = NULL;
		$pmpro_visible_levels = NULL;
	}

	// If use_cache is true check if we have something in a static var.
	if ( $include_hidden && isset( $pmpro_all_levels ) ) {
		$pmpro_levels = $pmpro_all_levels;
		return $pmpro_all_levels;
	}
	if ( ! $include_hidden && isset( $pmpro_visible_levels ) ) {
		$pmpro_levels = $pmpro_visible_levels;
		return $pmpro_visible_levels;
	}

	// build query
	$sqlQuery = "SELECT * FROM $wpdb->pmpro_membership_levels ";
	if ( ! $include_hidden ) {
		$sqlQuery .= ' WHERE allow_signups = 1 ORDER BY id';
	}

	// get levels from the DB
	$raw_levels = $wpdb->get_results( $sqlQuery );

	// lets put them into an array where the key is the id of the level
	$pmpro_levels = array();
	foreach ( $raw_levels as $raw_level ) {
		$raw_level->initial_payment = pmpro_round_price( $raw_level->initial_payment );
		$raw_level->billing_amount = pmpro_round_price( $raw_level->billing_amount );
		$raw_level->trial_amount = pmpro_round_price( $raw_level->trial_amount );
		$pmpro_levels[ $raw_level->id ] = $raw_level;
	}

	// Store an extra cache specific to the include_hidden param.
	if ( $include_hidden ) {
		$pmpro_all_levels = $pmpro_levels;
	} else {
		$pmpro_visible_levels = $pmpro_levels;
	}
	return $pmpro_levels;
}

/**
 * Check if any level(s) are available for signup.
 * @return bool
 * @since 2.3
 */
function pmpro_are_any_visible_levels() {
	$levels = pmpro_getAllLevels( false, true );

	if ( ! empty( $levels ) ) {
		$r = true;
	} else {
		$r = false;
	}

	return $r;
}

/**
 * Get level at checkout.
 * 
 * This function is only meant to be called once during checkout. Afterwards, the
 * checkout level object should be passed to relevent hooks/filters.
 *
 * If no level is passed or found in the URL parameters, global vars,
 * or in the post options, then this will return the first level found.
 *
 * @param int $level_id (optional) Pass a level ID to force that level.
 * @param string $discount_code (optional) Pass a discount code to force that code
 *
 * @return mixed|void
 */
function pmpro_getLevelAtCheckout( $level_id = null, $discount_code = null ) {
	global $pmpro_level, $wpdb, $post;

	// Reset $pmpro_level global.
	$pmpro_level = null;

	// Default to level passed in via URL.
	if ( empty( $level_id ) && ! empty( $_REQUEST['pmpro_level'] ) ) {
		$level_id = intval( $_REQUEST['pmpro_level'] );
	}

	// If we don't have a level, check the legacy 'level' request parameter.
	if ( empty( $level_id ) && ! empty( $_REQUEST['level'] ) ) {
		// TODO: We may want to show a message here that the level parameter is deprecated.
		$level_id = intval( $_REQUEST['level'] );
	}

	// If we still don't have a level yet, check for a default level in the custom fields for this post.
	if ( empty( $level_id ) && ! empty( $post ) ) {
		$level_id = intval( get_post_meta( $post->ID, 'pmpro_default_level', true ) );
	}

	// If we still don't have a level, use the default level.
	if ( empty( $level_id ) ) {
		$all_levels = pmpro_getAllLevels( false, false );

		if ( ! empty( $all_levels ) ) {
			// Get lowest level ID.
			$default_level =  min( array_keys( $all_levels ) );
		} else {
			$default_level = null;
		}

		$level_id = apply_filters( 'pmpro_default_level', intval( $default_level ) );
	}

	// If we still don't have a level, bail.
	if ( empty( $level_id ) || intval( $level_id ) < 1 ) {
		return;
	}

	// default to discount code passed in via URL.
	if ( empty( $discount_code ) && ! empty( $_REQUEST['pmpro_discount_code'] ) ) {
		$discount_code = preg_replace( '/[^A-Za-z0-9\-]/', '', sanitize_text_field( $_REQUEST['pmpro_discount_code'] ) );
	}

	// If we still don't have a discount code, check the legacy 'discount_code' request parameter.
	if ( empty( $discount_code ) && ! empty( $_REQUEST['discount_code'] ) ) {
		$discount_code = preg_replace( '/[^A-Za-z0-9\-]/', '', sanitize_text_field( $_REQUEST['discount_code'] ) );
	}

	// If we still don't have a discount code, add a filter to let other plugins add one.
	if ( empty( $discount_code ) ) {
		$discount_code = apply_filters( 'pmpro_default_discount_code', null, $level_id );
	}

	// If we are using a discount code, check it and get the level.
	if ( ! empty( $level_id ) && ! empty( $discount_code ) ) {
		$discount_code_id = $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_discount_codes WHERE code = '" . esc_sql( $discount_code ) . "' LIMIT 1" );

		// check code
		$code_check = pmpro_checkDiscountCode( $discount_code, $level_id, true );
		if ( $code_check[0] != false ) {
			$sqlQuery    = "SELECT l.id, cl.*, l.name, l.description, l.allow_signups, l.confirmation FROM $wpdb->pmpro_discount_codes_levels cl LEFT JOIN $wpdb->pmpro_membership_levels l ON cl.level_id = l.id LEFT JOIN $wpdb->pmpro_discount_codes dc ON dc.id = cl.code_id WHERE dc.code = '" . esc_sql( $discount_code ) . "' AND cl.level_id = '" . esc_sql( $level_id ) . "' LIMIT 1";
			$pmpro_level = $wpdb->get_row( $sqlQuery );

			// if the discount code doesn't adjust the level, let's just get the straight level
			if ( empty( $pmpro_level ) ) {
				$pmpro_level = $wpdb->get_row( "SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . esc_sql( $level_id ) . "' LIMIT 1" );
			}

			// filter adjustments to the level
			$pmpro_level->code_id       = $discount_code_id;
			$pmpro_level->discount_code = $discount_code;
			$pmpro_level                = apply_filters( 'pmpro_discount_code_level', $pmpro_level, $discount_code_id );
		} else {
			// error with discount code, we want to halt checkout
			pmpro_setMessage( $code_check[1], 'pmpro_error' );
		}
	}

	// If we don't have a level object yet, pull it from the database.
	if ( empty( $pmpro_level ) && ! empty( $level_id ) ) {
		$pmpro_level = $wpdb->get_row( "SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . esc_sql( $level_id ) . "' AND allow_signups = 1 LIMIT 1" );
	}

	// Filter the level (for upgrades, etc).
	$pmpro_level = apply_filters( 'pmpro_checkout_level', $pmpro_level );

	return $pmpro_level;
}

/**
 * Get an ordered list of level objects or level IDs.
 *
 * @param array $pmpro_levels An array of level objects or level IDs to be reordered.
 * @return array $pmpro_levels An ordered array of level objects or level IDs.
 *
 */
function pmpro_sort_levels_by_order( $pmpro_levels ) {
	$pmpro_level_order = get_option( 'pmpro_level_order' );

	// No custom sort order, just return.
	if ( empty( $pmpro_level_order ) ) {
		return $pmpro_levels;
	}

	// Convert the level order option to an array.
	$sort_order  = explode( ',',$pmpro_level_order );

	// Reorder the array.
	$reordered_levels = array();
	foreach ( $sort_order as $level_id ) {
		foreach ( $pmpro_levels as $key => $level ) {
			if ( ! empty ( $level->id ) && $level_id == $level->id ) {
				$reordered_levels[$level_id] = $pmpro_levels[$key];
			} elseif ( ! empty( $level ) && is_string( $level ) && $level_id == $level ) {
				$reordered_levels[$level_id] = $pmpro_levels[$key];
			}
		}
	}
	$pmpro_levels = $reordered_levels;

	return $pmpro_levels;
}

function pmpro_getCheckoutButton( $level_id, $button_text = null, $classes = null ) {
	if ( ! empty( $level_id ) ) {
		// get level
		$level = pmpro_getLevel( $level_id );

		$level_id = intval( $level_id );

		if( ! empty( $level ) ) {

			// default button text with name field for replacement
			if ( empty( $button_text ) ) {
				$button_text = esc_html__( 'Sign Up for !!name!! Now', 'paid-memberships-pro' );
			}

			// replace vars - this will be escaped when outputting (see below).
			$replacements = array(
				'!!id!!' => $level->id,
				'!!name!!' => $level->name,
				'!!description!!' => $level->description,
				'!!confirmation!!' => $level->confirmation,
				'!!initial_payment!!' => pmpro_filter_price_for_text_field( $level->initial_payment ),
				'!!billing_amount!!' => pmpro_filter_price_for_text_field( $level->billing_amount ),
				'!!cycle_number!!' => $level->cycle_number,
				'!!cycle_period!!' => $level->cycle_period,
				'!!billing_limit!!' => $level->billing_limit,
				'!!trial_amount!!' => pmpro_filter_price_for_text_field( $level->trial_amount ),
				'!!trial_limit!!' => $level->trial_limit,
				'!!expiration_number!!' => $level->expiration_number,
				'!!expiration_period!!' => $level->expiration_period,
			);
			$button_text = str_replace( array_keys( $replacements ), $replacements, $button_text );
		}
	}

	if ( empty( $button_text ) ) {
		$button_text = esc_html__( 'Sign Up Now', 'paid-memberships-pro' );
	}

	if ( empty( $classes ) ) {
		$classes = 'pmpro_btn';
	}

	if ( ! empty( $level_id ) ) {
		$r = '<a href="' . esc_url( pmpro_url( 'checkout', '?pmpro_level=' . $level_id ) ) . '" class="' . esc_attr( $classes ) . '">' . wp_kses_post( $button_text ) . '</a>';
	} else {
		$r = '<a href="' . esc_url( pmpro_url( 'checkout' ) ) . '" class="' . esc_attr( $classes ) . '">' . wp_kses_post( $button_text ) . '</a>';
	}

	return $r;
}

/**
 * Get the "domain" from a URL. By domain, we mean the host name, minus any subdomains. So just the domain and TLD.
 *
 * @param string $url The URL to parse. (generally pass site_url() in WP)
 * @return string The domain.
 */
function pmpro_getDomainFromURL( $url = null ) {
	$domainparts = parse_url( $url );
	$domainparts = explode( '.', $domainparts['host'] );
	if ( count( $domainparts ) > 1 ) {
		// check for ips
		$isip = true;
		foreach ( $domainparts as $part ) {
			if ( ! is_numeric( $part ) ) {
				$isip = false;
				break;
			}
		}

		if ( $isip ) {
			// ip, e.g. 127.1.1.1
			$domain = implode( '.', $domainparts );
		} else {
			// www.something.com, etc.
			$domain = $domainparts[ count( $domainparts ) - 2 ] . '.' . $domainparts[ count( $domainparts ) - 1 ];
		}
	} else {
		// localhost or another single word domain
		$domain = $domainparts[0];
	}

	return $domain;
}

if ( ! function_exists( 'pmpro_getMemberStartdate' ) ) {
	/**
	 * Get a member's start date... either in general or for a specific level_id.
	 */
	function pmpro_getMemberStartdate( $user_id = null, $level_id = 0 ) {
		if ( empty( $user_id ) ) {
			global $current_user;
			$user_id = $current_user->ID;
		}

		// make sure user and level id are int for security
		$user_id = intval( $user_id );
		$level_id = intval( $level_id );

		global $pmpro_startdates;   // for cache
		if ( empty( $pmpro_startdates[ $user_id ][ $level_id ] ) ) {
			global $wpdb;

			if ( ! empty( $level_id ) ) {
				$sqlQuery = "SELECT UNIX_TIMESTAMP(CONVERT_TZ(startdate, '+00:00', @@global.time_zone)) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND membership_id IN(" . (int) $level_id . ") AND user_id = '" . $user_id . "' ORDER BY id LIMIT 1";
			} else {
				$sqlQuery = "SELECT UNIX_TIMESTAMP(CONVERT_TZ(startdate, '+00:00', @@global.time_zone)) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND user_id = '" . esc_sql( $user_id ) . "' ORDER BY id LIMIT 1";
			}

			$startdate = apply_filters( 'pmpro_member_startdate', $wpdb->get_var( $sqlQuery ), $user_id, $level_id );

			$pmpro_startdates[ $user_id ][ $level_id ] = $startdate;
		}

		return $pmpro_startdates[ $user_id ][ $level_id ];
	}
}

if ( ! function_exists( 'pmpro_getMemberDays' ) ) {
	/**
	 * How long has this member been a member.
	 */
	function pmpro_getMemberDays( $user_id = null, $level_id = 0 ) {
		if ( empty( $user_id ) ) {
			global $current_user;
			$user_id = $current_user->ID;
		}

		global $pmpro_member_days;
		if ( empty( $pmpro_member_days[ $user_id ][ $level_id ] ) ) {
			$startdate = pmpro_getMemberStartdate( $user_id, $level_id );

			// check that there was a startdate at all
			if ( empty( $startdate ) ) {
				$pmpro_member_days[ $user_id ][ $level_id ] = 0;
			} else {
				$now = current_time( 'timestamp' );
				$days = ( $now - $startdate ) / 3600 / 24;

				$pmpro_member_days[ $user_id ][ $level_id ] = $days;
			}
		}

		return $pmpro_member_days[ $user_id ][ $level_id ];
	}
}

// the start of a message handling script
function pmpro_setMessage( $message, $type, $force = false ) {
	global $pmpro_msg, $pmpro_msgt;

	// for now, we only show the first message generated
	if ( $force || empty( $pmpro_msg ) ) {
		$pmpro_msg = apply_filters( 'pmpro_set_message', $message, $type );
		$pmpro_msgt = $type;
	}
}

/**
 * Show a PMPro message set via pmpro_setMessage
 *
 * @since 1.8.5
 */
function pmpro_showMessage() {
	global $pmpro_msg, $pmpro_msgt;

	$allowed_html = array (
		'a' => array (
			'href' => array(),
			'target' => array(),
			'title' => array(),
		),
		'em' => array(),
		'p' => array(),
		'span' => array(
			'class' => array(),
		),
		'strong' => array(),
		'ul' => array(),
		'li' => array(),
	);

	if ( ! empty( $pmpro_msg ) ) {
		?>
		<div role="alert" id="pmpro_message" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_message ' . $pmpro_msgt, $pmpro_msgt ) ); ?>">
			<p><?php echo wp_kses( $pmpro_msg, $allowed_html ); ?></p>
		</div>
		<?php
	}
}

/**
 * Return all CSS class names for the specified element and allow custom class names to be used via filter.
 *
 * @since 2.3.4
 *
 * @param  mixed  $class A string or array of element class names.
 * @param  string $element The element to return class names for.
 *
 * @return string $class A string of class names separated by spaces.
 *
 */
function pmpro_get_element_class( $class, $element = null ) {
	if ( empty( $element ) ) {
		$element = $class;
	}

	// Convert class values to an array.
	if ( ! is_array( $class ) ) {
		$class = explode( ' ', trim( $class ) );
	}

	// Escape elements of the array of class names.
	$class = array_map( 'esc_attr', $class );

	/**
	 * Filters the list of CSS class names for the current element.
	 *
	 * @since 2.3.4
	 *
	 * @param array  $class An array of element class names.
	 * @param string  $element The element to return class names for.
	 */
	$class = apply_filters( 'pmpro_element_class', $class, $element );

	if ( ! empty( $class ) ) {
		$class = array_unique( $class );
		return implode( ' ', $class );
	} else {
		return '';
	}
}

/**
 * Return field state-specific CSS class names for the field.
 *
 * @since 2.3.4
 *
 * Callback for the pmpro_element_class filter.
 */
function pmpro_get_field_class( $class, $element ) {
	global $pmpro_error_fields, $pmpro_required_billing_fields, $pmpro_required_user_fields;

	// error on this field?
	if ( ! empty( $pmpro_error_fields ) && in_array( $element, $pmpro_error_fields ) ) {
		$class[] = 'pmpro_form_input-error';
	}

	if ( is_array( $pmpro_required_billing_fields ) && is_array( $pmpro_required_user_fields ) ) {
		$required_fields = array_merge( array_keys( $pmpro_required_billing_fields ), array_keys( $pmpro_required_user_fields ) );
	} elseif ( is_array( $pmpro_required_billing_fields ) ) {
		$required_fields = array_keys( $pmpro_required_billing_fields );
	} elseif ( is_array( $pmpro_required_user_fields ) ) {
		$required_fields = array_keys( $pmpro_required_user_fields );
	} else {
		$required_fields = array();
	}

	// required?
	if ( in_array( $element, $required_fields ) ) {
		$class[] = 'pmpro_form_input-required';
	}

	// DEPRECATED: Use pmpro_element_class to filter classes instead.
	$class = apply_filters( 'pmpro_field_classes', $class, $element );

	return $class;
}
add_filter( 'pmpro_element_class', 'pmpro_get_field_class', 10, 2 );

/**
 * Get a var from $_GET or $_POST.
 */
function pmpro_getParam( $index, $method = 'REQUEST', $default = '', $sanitize_function = 'sanitize_text_field' ) {
	// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	if ( $method == 'REQUEST' ) {
		if ( ! empty( $_REQUEST[ $index ] ) ) {
			return call_user_func( $sanitize_function, $_REQUEST[ $index ] );
		}
	} elseif ( $method == 'POST' ) {
		if ( ! empty( $_POST[ $index ] ) ) {
			return call_user_func( $sanitize_function, $_POST[ $index ] );
		}
	} elseif ( $method == 'GET' ) {
		if ( ! empty( $_GET[ $index ] ) ) {
			return call_user_func( $sanitize_function, $_GET[ $index ] );
		}
	}
	// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

	return $default;
}

/**
 * Format an address from address, city, state, zip, country, and phone.
 */
function pmpro_formatAddress( $name, $address1, $address2, $city, $state, $zip, $country, $phone, $nl2br = true ) {
	$address = '';

	if ( ! empty( $name ) ) {
		$address .= $name . "\n";
	}

	if ( ! empty( $address1 ) ) {
		$address .= $address1 . "\n";
	}

	if ( ! empty( $address2 ) ) {
		$address .= $address2 . "\n";
	}

	if ( ! empty( $city ) && ! empty( $state ) ) {
		$address .= $city . ', ' . $state;

		if ( ! empty( $zip ) ) {
			$address .= ' ' . $zip;
		}

		$address .= "\n";
	} elseif ( ! empty( $city ) ) {
		$address .= $city;
		if ( ! empty( $zip ) ) {
			$address .= ' ' . $zip;
		}
		$address .= "\n";
	}

	if ( ! empty( $country ) ) {
		$address .= $country . "\n";
	}

	if ( ! empty( $phone ) ) {
		$address .= formatPhone( $phone );
	}

	if ( $nl2br ) {
		$address = nl2br( $address );
	}

	return apply_filters( 'pmpro_formatted_address', $address, $name, $address1, $address2, $city, $state, $zip, $country, $phone, $nl2br );
}

/**
 * Checks if all required settings are set.
 */
function pmpro_is_ready() {
	global $wpdb, $pmpro_pages, $pmpro_level_ready, $pmpro_gateway_ready, $pmpro_pages_ready;

	// check if there is at least one level
	$pmpro_level_ready = (bool) $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_membership_levels LIMIT 1" );

	// check if the gateway settings are good. first check if it's needed (is there paid membership level)
	$paid_membership_level = $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_membership_levels WHERE allow_signups = 1 AND (initial_payment > 0 OR billing_amount > 0 OR trial_amount > 0) LIMIT 1" );
	$paid_user_subscription = $wpdb->get_var( "SELECT user_id FROM $wpdb->pmpro_memberships_users WHERE initial_payment > 0 OR billing_amount > 0 OR trial_amount > 0 LIMIT 1" );

	if ( empty( $paid_membership_level ) && empty( $paid_user_subscription ) ) {
		// no paid membership level now or attached to a user. we don't need the gateway setup
		$pmpro_gateway_ready = true;
	} else {
		$gateway             = get_option( 'pmpro_gateway' );
		$gateway_environment = get_option( 'pmpro_gateway_environment' );
		if ( $gateway == 'authorizenet' ) {
			if ( $gateway_environment && get_option( 'pmpro_loginname' ) && get_option( 'pmpro_transactionkey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'paypalwpp' || $gateway == 'paypalexpress' ) {
			if ( $gateway_environment && get_option( 'pmpro_gateway_email' ) && get_option( 'pmpro_apiusername' ) && get_option( 'pmpro_apipassword' ) && get_option( 'pmpro_apisignature' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'paypalstandard' ) {
			if ( $gateway_environment && get_option( 'pmpro_gateway_email' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'payflowpro' ) {
			if ( get_option( 'pmpro_payflow_partner' ) && get_option( 'pmpro_payflow_vendor' ) && get_option( 'pmpro_payflow_user' ) && get_option( 'pmpro_payflow_pwd' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'stripe' ) {
			if ( $gateway_environment && get_option( 'pmpro_stripe_secretkey' ) && get_option( 'pmpro_stripe_publishablekey' ) ) {
				// Using legacy keys.
				$pmpro_gateway_ready = true;
			} elseif ( $gateway_environment && get_option( 'pmpro_' . $gateway_environment . '_stripe_connect_secretkey' ) && get_option( 'pmpro_' . $gateway_environment . '_stripe_connect_publishablekey' ) ) {
				// Using connect.
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'braintree' ) {
			if ( $gateway_environment && get_option( 'pmpro_braintree_merchantid' ) && get_option( 'pmpro_braintree_publickey' ) && get_option( 'pmpro_braintree_privatekey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'twocheckout' ) {
			if ( $gateway_environment && get_option( 'pmpro_twocheckout_apiusername' ) && get_option( 'pmpro_twocheckout_apipassword' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'cybersource' ) {
			if ( $gateway_environment && get_option( 'pmpro_cybersource_merchantid' ) && get_option( 'pmpro_cybersource_securitykey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'check' ) {
			$pmpro_gateway_ready = true;
		} else {
			$pmpro_gateway_ready = false;
		}
	}

	// check if we have all pages
	if ( $pmpro_pages['account'] &&
	   $pmpro_pages['billing'] &&
	   $pmpro_pages['cancel'] &&
	   $pmpro_pages['checkout'] &&
	   $pmpro_pages['confirmation'] &&
	   $pmpro_pages['invoice'] &&
	   $pmpro_pages['levels'] ) {
		$pmpro_pages_ready = true;
	} else {
		$pmpro_pages_ready = false;
	}

	// now check both
	if ( $pmpro_gateway_ready && $pmpro_pages_ready ) {
		$r = true;
	} else {
		$r = false;
	}

	/**
	 * Filter to determine if PMPro setup is complete or
	 * if notices or warnings need to be shown in the PMPro settings.
	 *
	 * Note: The filter should return true or false and also set
	 * the $pmpro_level_ready, $pmpro_gateway_ready, $pmpro_pages_ready global variabls.
	 *
	 * @since 1.8.4.5
	 *
	 * @param bool $r ready?
	 */
	$r = apply_filters( 'pmpro_is_ready', $r );

	return $r;
}

/**
 * Display the Setup Wizard links.
 *
 * @since 2.10
 *
 * @return bool $show Whether or not the Setup Wizard link should show.
 */
function pmpro_show_setup_wizard_link() {
	global $pmpro_ready;

	// If PMPro isn't ready AND the wizard hasn't completed yet.
	if ( ! $pmpro_ready && get_option( 'pmpro_wizard_step' ) !== 'done' ) {
		$show = true;
	} else {
		$show = false;
	}

	/**
	 * Filter to determine if the Setup Wizard link should show. Allows you to bypass whether or not to show the link.
	 */
	return apply_filters( 'pmpro_show_setup_wizard_link', $show );
}

/**
 * Display Order Price Data with Parts
 *
 * @param object $pmpro_order The full order object.
 * @param string $format Format of the return value. Accepts array, span, list, or line_breaks.
 *
 * @return array|string $price_parts The array or formatted HTML string to display price parts and total.
 *
 */
function pmpro_get_price_parts( $pmpro_order, $format = 'array' ) {
	$pmpro_price_parts = array();

	if ( ! empty( $pmpro_order->subtotal ) && $pmpro_order->subtotal != $pmpro_order->total ) {
		$pmpro_price_parts['subtotal'] = array(
			'label' => __( 'Subtotal', 'paid-memberships-pro' ),
			'value' => pmpro_escape_price( $pmpro_order->get_formatted_subtotal() ),
		);
	}

	if ( ! empty( $pmpro_order->tax ) ) {
		$pmpro_price_parts['tax'] = array(
			'label' => __( 'Tax', 'paid-memberships-pro' ),
			'value' => pmpro_escape_price( $pmpro_order->get_formatted_tax() ),
		);
	}

	/**
	 * Filter to modify the price parts, add parts, or modify the display. Does not include the order total.
	 *
	 * @param array $pmpro_price_parts The array of price parts not including the total.
	 * @param string $format Format of the return value passed to the function.
	 * @param object $pmpro_order The full order object.
	 *
	 * @return array $pmpro_price_parts Filtered array of price parts not including the total.
	 *
	 */
	$pmpro_price_parts = apply_filters( 'pmpro_get_price_parts', $pmpro_price_parts, $pmpro_order );

	$pmpro_price_parts_with_total = $pmpro_price_parts;

	if ( ! empty( $pmpro_order->total ) ) {
		$pmpro_price_parts_with_total['total'] = array(
			'label' => __( 'Total', 'paid-memberships-pro' ),
			'value' => pmpro_escape_price( $pmpro_order->get_formatted_total() ),
		);
	}

	/**
	 * Filter including the total price to modify the price parts, add parts, or modify the display.
	 *
	 * @param array $pmpro_price_parts The array of price parts including the total.
	 * @param string $format Format of the return value passed to the function.
	 * @param object $pmpro_order The full order object.
	 *
	 * @return array $pmpro_price_parts Filtered array of price parts not including the total.
	 *
	 */
	$pmpro_price_parts_with_total = apply_filters( 'pmpro_get_price_parts_with_total', $pmpro_price_parts_with_total, $pmpro_order );

	if ( $format == 'array' ) {
		return $pmpro_price_parts_with_total;
	} else {
		// Start building our formatted return string.
		$pmpro_price = '';
		if ( $format == 'span' ) {
			foreach ( $pmpro_price_parts_with_total as $key => $pmpro_price_part ) {
				$pmpro_price .= '<span class="' . pmpro_get_element_class( 'pmpro_price_part_span pmpro_price_part-' . sanitize_html_class( $key ), 'pmpro_price_part-' . sanitize_html_class( $key ) ) . '"><span class="' . pmpro_get_element_class( 'pmpro_price_part_label' ) . '">' . esc_html( $pmpro_price_part['label'] ) . '</span> <span class="' . pmpro_get_element_class( 'pmpro_price_part_price' ) . '">' . esc_html( $pmpro_price_part['value'] ) . '</span></span>';
			}
		} elseif ( $format == 'list' ) {
			$pmpro_price .= '<ul class="' . pmpro_get_element_class( 'pmpro_price_part_list' ) . '">';
			foreach ( $pmpro_price_parts_with_total as $key => $pmpro_price_part ) {
				$pmpro_price .= '<li class="' . pmpro_get_element_class( 'pmpro_price_part-' . sanitize_html_class( $key ), 'pmpro_price_part-' . sanitize_html_class( $key ) ) . '"><span class="' . pmpro_get_element_class( 'pmpro_price_part_label' ) . '">' . esc_html( $pmpro_price_part['label'] ) . '</span> <span class="' . pmpro_get_element_class( 'pmpro_price_part_price' ) . '">' . esc_html( $pmpro_price_part['value'] ) . '</span></li>';
			}
		} else {
			// Default to each line separate by breaks.
			foreach ( $pmpro_price_parts_with_total as $key => $pmpro_price_part ) {
				$pmpro_price .= '<span class="' . pmpro_get_element_class( 'pmpro_price_part-' . sanitize_html_class( $key ), 'pmpro_price_part-' . sanitize_html_class( $key ) ) . '"><span class="' . pmpro_get_element_class( 'pmpro_price_part_label' ) . '">' . esc_html( $pmpro_price_part['label'] ) . '</span> <span class="' . pmpro_get_element_class( 'pmpro_price_part_price' ) . '">' . esc_html( $pmpro_price_part['value'] ) . '</span></span><br />';
			}
		}
	}
	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( (float) $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, sized to the currency's decimal count.
	$price_info['parts']['decimal_string'] = $currency_info['decimals'] > 0
		? sprintf( '%0' . $currency_info['decimals'] . 'd', $price_info['parts']['decimal'] )
		: '';

	// Get the amount as a string. Zero-decimal currencies have no fractional part.
	$price_info['amount_string'] = $currency_info['decimals'] > 0
		? sprintf( '%s.%s', $price_info['parts']['number'], $price_info['parts']['decimal_string'] )
		: (string) $price_info['parts']['number'];

	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
	$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.
 *
 * @since 3.4 Marking the $field argument as deprecated.
 *
 * @param array|int|null|string|stdClass $value The value to sanitize
 *
 * @return array|int|string|object     Sanitized value
 */
function pmpro_sanitize( $value, $field = null ) {
	if ( null !== $field ) {
		// This argument is deprecated. User fields now have sanitization logic in the field class.
		_deprecated_argument( __FUNCTION__, '3.4', esc_html__( 'The $field argument is deprecated. The sanitization logic is now built into the PMPro_Field class.', 'paid-memberships-pro' ) );
	}

	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, $wp_query;

	// 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.
	if ( ! empty( $wp_query ) ) {
		$queried_object = get_queried_object();
	} else {
		$queried_object = null;
	}

	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
  * @deprecated 3.2
  * @return mixed $order Order object.
  */
 function pmpro_build_order_for_checkout() {
	_deprecated_function( __FUNCTION__, '3.2' );

	global $gateway, $pmpro_level, $current_user, $bfirstname, $blastname, $baddress1, $baddress2, $bcity, $bstate, $bzipcode, $bcountry, $bphone, $bemail, $CardType, $AccountNumber, $ExpirationMonth, $ExpirationYear, $CVV;

	// Create a new order object.
	$morder                   = new MemberOrder();
	$morder->user_id          = $current_user->ID;
	$morder->membership_id    = $pmpro_level->id;
	$morder->cardtype         = $CardType;
	$morder->accountnumber    = $AccountNumber;
	$morder->expirationmonth  = $ExpirationMonth;
	$morder->expirationyear   = $ExpirationYear;
	$morder->gateway          = $gateway;
	$morder->billing          = new stdClass();
	$morder->billing->name    = $bfirstname . " " . $blastname;
	$morder->billing->street  = trim( $baddress1 );
	$morder->billing->street2 = trim( $baddress2 );
	$morder->billing->city    = $bcity;
	$morder->billing->state   = $bstate;
	$morder->billing->country = $bcountry;
	$morder->billing->zip     = $bzipcode;
	$morder->billing->phone   = $bphone;

	// Calculate the order subtotal, tax, and total.
	$morder->subtotal         = pmpro_round_price( $pmpro_level->initial_payment );
	$morder->tax              = pmpro_round_price( $morder->getTax( true ) );
	$morder->total            = pmpro_round_price( $morder->subtotal + $morder->tax );

	// Finish setting up the order.
	$morder->setGateway();
	$morder->getMembershipLevelAtCheckout();	

	// 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.
 *
 * @since 3.4.4 Added support for != and <>.
 *
 * @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;
		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
 * @since 3.7 Calling this function to read webhook status is deprecated.
 * @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 ) ) {
		if ( ! defined( 'PMPRO_DOING_WEBHOOK' ) ) {
			define( 'PMPRO_DOING_WEBHOOK', $gateway );
		}
		do_action( 'pmpro_doing_webhook', $gateway );
		return true;
	}

	// Reading webhook status through this function is deprecated.
	_deprecated_argument(
		__FUNCTION__,
		'3.7',
		esc_html__( 'Reading webhook status via pmpro_doing_webhook() is deprecated and will be removed in a future version of Paid Memberships Pro.', 'paid-memberships-pro' )
	);

	// 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.
 *
 * @param string|null $gateway Optional. The gateway the webhook was not handled for.
 *
 * @return void
 *
 * @since 2.8
 */
function pmpro_unhandled_webhook( $gateway = null ) {
	if ( null === $gateway ) {
		$gateway = defined( 'PMPRO_DOING_WEBHOOK' ) ? PMPRO_DOING_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', $gateway );
}

/**
 * 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( '@<script[^>]*?>.*?</script>@si', '', $sanitized_string );
	}

	// Preserve Liquid-style control tags through wp_kses() sanitization.
	// Only {% %} tags are preserved (conditions use < > operators that wp_kses encodes).
	// Output tags {{ }} are NOT preserved so wp_kses can still sanitize their contents.
	$liquid_placeholders = array();
	if ( 'pmpro_email' === $context ) {
		$placeholder_id = wp_rand();
		$sanitized_string = preg_replace_callback(
			'/(\{%.*?%\})/s',
			function ( $matches ) use ( &$liquid_placeholders, $placeholder_id ) {
				$placeholder = '%%PMPRO_LIQUID_' . $placeholder_id . '_' . count( $liquid_placeholders ) . '%%';
				$liquid_placeholders[ $placeholder ] = $matches[0];
				return $placeholder;
			},
			$sanitized_string
		);
	}

	$sanitized_string = wp_kses( $sanitized_string, $context );

	// Restore Liquid template tags.
	if ( ! empty( $liquid_placeholders ) ) {
		$sanitized_string = str_replace(
			array_keys( $liquid_placeholders ),
			array_values( $liquid_placeholders ),
			$sanitized_string
		);
	}

	/**
	 * 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 );
}

/**
 * Get TinyMCE autocomplete settings for Liquid syntax.
 *
 * @since 3.8
 *
 * @param array $variables Variables to include in autocomplete. Supports a flat variable => description map or grouped maps.
 * @return array Liquid autocomplete settings.
 */
function pmpro_get_liquid_autocomplete_settings( $variables = array() ) {
	$settings = array(
		'variables' => pmpro_get_liquid_autocomplete_variable_suggestions( $variables ),
		'filters'   => pmpro_get_liquid_autocomplete_filter_suggestions(),
		'tags'      => pmpro_get_liquid_autocomplete_tag_suggestions(),
		'strings'   => array(
			'autocompleteLabel' => __( 'Liquid autocomplete', 'paid-memberships-pro' ),
			'liquidTagsHeader' => __( 'Liquid Tags', 'paid-memberships-pro' ),
		),
	);

	return $settings;
}

/**
 * Convert a list of Liquid variables into autocomplete suggestions.
 *
 * @since 3.8
 *
 * @param array $variables Variables to include in autocomplete. Supports a flat variable => description map or grouped maps.
 * @return array Variable autocomplete suggestions.
 */
function pmpro_get_liquid_autocomplete_variable_suggestions( $variables ) {
	$suggestions = array();

	foreach ( (array) $variables as $key => $description ) {
		if ( is_array( $description ) ) {
			foreach ( $description as $group_key => $group_description ) {
				$suggestions[] = pmpro_get_liquid_autocomplete_variable_suggestion( $group_key, $group_description );
			}

			continue;
		}

		$suggestions[] = pmpro_get_liquid_autocomplete_variable_suggestion( $key, $description );
	}

	return $suggestions;
}

/**
 * Convert a single Liquid variable into an autocomplete suggestion.
 *
 * @since 3.8
 *
 * @param string $variable    The variable token.
 * @param string $description The variable description.
 * @return array Variable autocomplete suggestion.
 */
function pmpro_get_liquid_autocomplete_variable_suggestion( $variable, $description = '' ) {
	$name = $variable;

	if ( preg_match( '/^\{\{\s*([^}|\s]+)/', $variable, $match ) ) {
		$name = $match[1];
	}

	return array(
		'name'        => $name,
		'label'       => $variable,
		'description' => $description,
		'insert'      => $variable,
	);
}

/**
 * Get Liquid filter autocomplete suggestions.
 *
 * @since 3.8
 *
 * @return array Liquid filter autocomplete suggestions.
 */
function pmpro_get_liquid_autocomplete_filter_suggestions() {
	$suggestions = array();

	if ( ! class_exists( 'PMPro_Liquid_Renderer' ) ) {
		return $suggestions;
	}

	foreach ( PMPro_Liquid_Renderer::get_filters() as $filter_name => $filter ) {
		$suggestions[] = array(
			'name'        => $filter_name,
			'label'       => $filter_name,
			'description' => isset( $filter['description'] ) ? $filter['description'] : '',
			'insert'      => ( 'default' === $filter_name ) ? ' | default: "__pmpro_cursor__"' : ' | ' . $filter_name,
		);
	}

	return $suggestions;
}

/**
 * Get Liquid tag autocomplete suggestions.
 *
 * @since 3.8
 *
 * @return array Liquid tag autocomplete suggestions.
 */
function pmpro_get_liquid_autocomplete_tag_suggestions() {
	$suggestions = array(
		array(
			'name'        => 'if',
			'label'       => '{% if ... %} ... {% endif %}',
			'description' => __( 'Show content when a condition is true', 'paid-memberships-pro' ),
			'insert'      => '{% if __pmpro_cursor__ %}{% endif %}',
		),
		array(
			'name'        => 'elsif',
			'label'       => '{% elsif ... %}',
			'description' => __( 'Add another condition inside an if block', 'paid-memberships-pro' ),
			'insert'      => '{% elsif __pmpro_cursor__ %}',
		),
		array(
			'name'        => 'else',
			'label'       => '{% else %}',
			'description' => __( 'Add fallback content inside an if block', 'paid-memberships-pro' ),
			'insert'      => '{% else %}',
		),
		array(
			'name'        => 'endif',
			'label'       => '{% endif %}',
			'description' => __( 'Close an if block', 'paid-memberships-pro' ),
			'insert'      => '{% endif %}',
		),
	);

	return $suggestions;
}

/**
 * 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 );

	// Check if it's a valid IPv4 or IPv6 address.
	if ( ! filter_var( $client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && ! filter_var( $client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
		return false;
	}

	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 );
}

/**
 * When the pmpro_last_known_url option is updated, base64 encode it to
 * prevent string replacements from changing it when the site is migrated.
 *
 * @since 3.5
 * @link https://developer.wordpress.org/reference/hooks/pre_update_option_option/
 *
 * @param string $new_value The new value for the option.
 * @return string The encoded value for the option.
 */
function pmpro_encode_last_known_url( $new_value ) {
	// Only encode non-empty values.
	if ( ! empty( $new_value ) ) {
		$new_value = 'b64:' . base64_encode( $new_value );
	}
	return $new_value;
}
add_filter( 'pre_update_option_pmpro_last_known_url', 'pmpro_encode_last_known_url' );

/**
 * When the pmpro_last_known_url option is retrieved, if it
 * is base64 encoded, decode it.
 *
 * @since 3.5
 * @link https://developer.wordpress.org/reference/hooks/option_option/
 *
 * @param string $value The value of the option.
 * @return string The decoded value of the option.
 */
function pmpro_decode_last_known_url( $value ) {
	// Check if the value is base64 encoded.
	if ( strpos( $value, 'b64:' ) === 0 ) {
		$value = base64_decode( substr( $value, 4 ) );
	} else {
		// If the value is not encoded, then it is an old value. Encode it now.
		remove_filter( 'option_pmpro_last_known_url', 'pmpro_decode_last_known_url' );
		update_option( 'pmpro_last_known_url', $value );
		add_filter( 'option_pmpro_last_known_url', 'pmpro_decode_last_known_url' );
	}
	return $value;
}
add_filter( 'option_pmpro_last_known_url', 'pmpro_decode_last_known_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|null $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 ) {
	// 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.
	$field = PMPro_Field_Group::get_field( $file_index );
	if ( ! empty( $field) ) {
		// 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' ) );
		}

		// Check the file type against the allowed types.
		$allowed_mime_types = ! empty( $field->allowed_file_types ) ? array_map( 'sanitize_text_field', explode( ',', $field->allowed_file_types ) ) : array();

		//Remove fullstops from the beginning of the allowed file types.
		$allowed_mime_types = array_map( function( $type ) {
			return ltrim( $type, '.' );
		}, $allowed_mime_types );

		// Check the file type against the allowed types. If empty allowed mimes, assume any file upload is okay.
		if ( ! empty( $allowed_mime_types ) && ! in_array( $filetype['ext'], $allowed_mime_types ) ) {
			/* translators: %s: list of allowed file types */
			return new WP_Error( 'pmpro_upload_file_type_error', sprintf( esc_html__( 'Invalid file type. Please try uploading the file type(s): %s', 'paid-memberships-pro' ), implode( ',' ,$allowed_mime_types ) ) );
		}
		
		// Check if the file upload is too big to upload.
		if ( $field->max_file_size > 0 ) {
			$upload_max_file_size_in_bytes = $field->max_file_size * 1024 * 1024;
			if ( $file['size'] > $upload_max_file_size_in_bytes ) {
				/* translators: 1: field label, 2: maximum file size in MB */
				return new WP_Error( 'pmpro_upload_file_size_error', sprintf( esc_html__( 'File size is too large for %s. Please upload files smaller than %dMB.', 'paid-memberships-pro' ), $field->label, $field->max_file_size ) );
			}
		}
	} else {
		/**
		 * 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;
}

/**
 * Function to convert a hex color to HSL.
 */
function pmpro_hex_to_hsl_parts( $hex ) {
	// Remove the # from the hex value, if present.
	$hex = str_replace( '#', '', $hex );

	$red = hexdec( substr( $hex, 0, 2 ) ) / 255;
	$green = hexdec( substr( $hex, 2, 2 ) ) / 255;
	$blue = hexdec( substr( $hex, 4, 2 ) ) / 255;

	$cmin = min( $red, $green, $blue );
	$cmax = max( $red, $green, $blue );
	$delta = $cmax - $cmin;

	if ( $delta == 0 ) {
		$hue = 0;
	} elseif ( $cmax === $red ) {
		$hue = ( ( $green - $blue ) / $delta );
	} elseif ( $cmax === $green ) {
		$hue = ( $blue - $red ) / $delta + 2;
	} else {
		$hue = ( $red - $green ) / $delta + 4;
	}

	$hue = round( $hue * 60 );
	if ( $hue < 0 ) {
		$hue += 360;
	}

	$lightness = ( ( $cmax + $cmin ) / 2 );
	$saturation = $delta === 0 ? 0 : ( $delta / ( 1 - abs( 2 * $lightness - 1 ) ) );
	if ( $saturation < 0 ) {
		$saturation += 1;
	}

	$lightness = round( $lightness * 100 );
	$saturation = round( $saturation * 100 );

 	return array( $hue, $saturation, $lightness );
}

/**
 * Calculate the end date for the period of time this order covers in the subscription.
 *
 * @since 3.1
 *
 * @param MemberOrder $order       The order to calculate the end date date for.
 * @param string      $date_format The format to use when formatting the profile start date.
 *
 * @return string The order's subscription period end date in UTC time and the desired $date_format.
 */
function pmpro_get_subscription_period_end_date_for_order( $order, $date_format ) {
	global $wpdb;

	// Get the subscription for this order.
	$subscription = $order->get_subscription();

	// If the order is not part of a subscription, return false.
	if ( empty( $subscription ) ) {
		return false;
	}

	// Get all orders for the subscription.
	$subscription_orders = $subscription->get_orders(
		array(
			'orderby' => '`timestamp` ASC, `id` ASC',
			'limit' => 1000,
			'status' => array( 'success', 'pending', 'refunded' ),
		)
	);

	// Get the timestamp of the order following the order passed to this function.
	$period_end = null;
	foreach ( $subscription_orders as $subscription_order ) {
		if ( $subscription_order->timestamp > $order->timestamp ) {
			$period_end = $subscription_order->timestamp;
			break;
		}
	}

	// If we don't have a date yet, this is the last order in the subscription.
	if ( empty( $period_end ) ) {
		if ( $subscription->get_status() === 'active' ) {
			// The subscription is active. Next payment date is the end date.
			$period_end = $subscription->get_next_payment_date( 'timestamp' );
		} else {
			// Subscription is not active. End date is the subscription end date.
			// Note, this can sometimes be the same date as the order date. We may consider to snapshot what would have been the next payment date in the future.
			$period_end = $subscription->get_enddate( 'timestamp' );
		}
	}

	// Format and return the end date.
	return date_i18n( $date_format, $period_end );
}

/**
 * Check if a method is specifically defined in the given object's class, not inherited.
 *
 * @since 3.2.2
 * @param object $object The object to check for the method.
 * @param string $method_name The name of the method to check.
 * @return bool True if the method is overridden in the object's class, false otherwise.
 */
function pmpro_method_defined_in_class( $object, $method_name ) {
    // Get the class of the object.
    $reflection_class = new ReflectionClass( $object );

    // Check if the method exists in this class.
    if ( !$reflection_class->hasMethod( $method_name ) ) {
        return false; // The method doesn't exist at all.
    }

    // Get the method reflection.
    $method = $reflection_class->getMethod( $method_name );

    // Check if the method's declaring class is the same as the object's class.
    return $method->getDeclaringClass()->getName() === $reflection_class->getName();
}

/**
 * Check if we can check a token order for completion.
 *
 * @since 3.3.3
 *
 * @param int $order_id The ID of the order to check.
 * @return bool True if we can check the order for completion, false otherwise.
 */
function pmpro_can_check_token_order_for_completion( $order_id ) {
	// Get the order object.
	$order = new MemberOrder( $order_id );

	// If the order does not exist, we can't check it.
	if ( empty( $order->id ) ) {
		return false;
	}

	// If the order is not a token order, we can't check it.
	if ( 'token' !== $order->status ) {
		return false;
	}

	// If the order does not have a gateway set, we can't check it.
	if ( empty( $order->Gateway ) ) {
		return false;
	}

	// Check if the order supports checking for completion.
	return $order->Gateway->supports( 'check_token_orders' );
}

/**
 * Check a token order for completion.
 *
 * @since 3.3.3
 *
 * @param int $order_id The ID of the order to check.
 * @return true|string True if the payment has been completed and the order processed. A string if an error occurred.
 */
function pmpro_check_token_order_for_completion( $order_id ) {
	// Get the order object.
	$order = new MemberOrder( $order_id );

	// If the order does not exist, we can't check it.
	if ( empty( $order->id ) ) {
		return __( 'Order not found.', 'paid-memberships-pro' );
	}

	// If the order is not a token order, we can't check it.
	if ( 'token' !== $order->status ) {
		return __( 'Order is not a token order.', 'paid-memberships-pro' );
	}

	// If the order does not have a gateway set, we can't check it.
	if ( empty( $order->Gateway ) ) {
		return __( 'Order gateway not found.', 'paid-memberships-pro' );
	}

	// Check the order for completion.
	return $order->Gateway->check_token_order( $order );
}

/**
 * Show a message on the account page for a specific membership level.
 */
function pmpro_display_member_account_level_message( $level ) {
	$membership_account_message = get_pmpro_membership_level_meta( $level->id, 'membership_account_message', true );
	if ( $membership_account_message ) {
		?>
		<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_account-membership-message' ) ); ?>">
			<?php echo wp_kses_post( do_shortcode( shortcode_unautop( wpautop( $membership_account_message ) ) ) ); ?>
		</div>
		<?php
	}
}
add_action( 'pmpro_membership_account_after_level_card_content', 'pmpro_display_member_account_level_message' );

/**
 * Update the level restrictions for a post.
 *
 * @since 3.6
 *
 * @param int $post_id The ID of the post to update.
 * @param array $level_ids The IDs of the levels to restrict the post to.
 */
function pmpro_update_post_level_restrictions( $post_id, $level_ids ) {
	global $wpdb;

	// Get the current level IDs for the post.
	$current_level_ids = $wpdb->get_col( $wpdb->prepare(
		"SELECT membership_id FROM {$wpdb->pmpro_memberships_pages} WHERE page_id = %d",
		intval( $post_id )
	) );

	// Get the list of level IDs to remove.
	$level_ids_to_remove = array_diff( $current_level_ids, $level_ids );
	if ( ! empty( $level_ids_to_remove ) ) {
		// Delete the restrictions for the levels that are being removed.
		foreach( $level_ids_to_remove as $level_id ) {
			$wpdb->delete(
				$wpdb->pmpro_memberships_pages,
				array(
					'membership_id' => intval( $level_id ),
					'page_id'       => intval( $post_id ),
				),
				array(
					'%d',
					'%d',
				)
			);
		}
	}

	// Get the list of level IDs to insert.
	$level_ids_to_insert = array_diff( $level_ids, $current_level_ids );
	if ( ! empty( $level_ids_to_insert ) ) {
		// Insert the restrictions for the new levels.
		foreach ( $level_ids_to_insert as $level_id ) {
			$wpdb->insert(
				$wpdb->pmpro_memberships_pages,
				array(
					'membership_id' => intval( $level_id ),
					'page_id'       => intval( $post_id ),
				),
				array(
					'%d',
					'%d',
				)
			);
		}
	}

	// If we made changes, run an action.
	if ( ! empty( $level_ids_to_remove ) || ! empty( $level_ids_to_insert ) ) {
		/**
		 * Action triggered after updating post level restrictions.
		 *
		 * @since 3.6
		 *
		 * @param int $post_id The ID of the post that was updated.
		 */
		do_action( 'pmpro_after_updating_post_level_restrictions', $post_id );
	}
}
