<?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/twentytwelve/functions.php
You can also create a custom plugin to place customization code into. Instructions are here:
http://www.paidmembershipspro.com/2012/08/create-a-plugin-for-pmpro-customizations/

Further documentation for customizing Paid Memberships Pro can be found here:
http://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_membership_ordermeta = $wpdb->prefix . 'pmpro_membership_ordermeta';
}
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 );
}

function pmpro_getOption( $s, $force = false ) {
	if ( get_option( 'pmpro_' . $s ) ) {
		return get_option( 'pmpro_' . $s );
	} else {
		return '';
	}
}

function pmpro_setOption( $s, $v = null, $sanitize_function = 'sanitize_text_field', $autoload = false ) {
	// no value is given, set v to the p var
	if ( $v === null && isset( $_POST[ $s ] ) ) {
		if ( is_array( $_POST[ $s ] ) ) {
			$v = array_map( $sanitize_function, $_POST[ $s ] );
		} else {
			$v = call_user_func( $sanitize_function, $_POST[ $s ] );
		}
	}

	if ( is_array( $v ) ) {
		$v = implode( ',', $v );
	} else {
		$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;
}

/**
 * Loads a template from one of the default paths (PMPro plugin or theme), or from filtered path
 *
 * @param null   $page_name - Name of the page/template
 * @param string $where - `local` or `url` (whether to load from FS or over http)
 * @param string $type - Type of template (valid: 'email' or 'pages', 'adminpages', 'preheader')
 * @param string $ext - File extension ('php', 'html', 'htm', etc)
 * @return string - The HTML for the template.
 *
 * TODO - Allow localized template files to be loaded?
 *
 * @since 1.8.9
 */
function pmpro_loadTemplate( $page_name = null, $where = 'local', $type = 'pages', $ext = 'php' ) {
	// called from page handler shortcode
	if ( is_null( $page_name ) ) {
		global $pmpro_page_name;
		$page_name = $pmpro_page_name;
	}

	if ( $where == 'local' ) {
		// template paths in order of priority (array gets reversed)
		$default_templates = array(
			PMPRO_DIR . "/{$type}/{$page_name}.{$ext}", // default plugin path
			get_template_directory() . "/paid-memberships-pro/{$type}/{$page_name}.{$ext}", // parent theme
			get_stylesheet_directory() . "/paid-memberships-pro/{$type}/{$page_name}.{$ext}", // child / active theme
		);
	} elseif ( $where == 'url' ) {
		// template paths in order of priority (array gets reversed)
		$default_templates = array(
			PMPRO_URL . "/{$type}/{$page_name}.{$ext}", // default plugin path
			get_template_directory_uri() . "/paid-memberships-pro/{$type}/{$page_name}.{$ext}", // parent theme
			get_stylesheet_directory_uri() . "/paid-memberships-pro/{$type}/{$page_name}.{$ext}", // child / active theme
		);

	}
	// Valid types: 'email', 'pages'
	$templates = apply_filters( "pmpro_{$type}_custom_template_path", $default_templates, $page_name, $type, $where, $ext );
	$user_templates = array_diff( $templates, $default_templates );
	$allowed_default_templates = array_intersect( $templates, $default_templates );

	// user specified a custom template path, so it has priority.
	if ( ! empty( $user_templates ) ) {
		$templates = array_merge($allowed_default_templates, $user_templates);
	}

	// last element included in the array is the most first one we try to load
	$templates = array_reverse( $templates );

	// look for template file to include
	ob_start();
	foreach ( $templates as $template_path ) {
		// If loading a local file, check if it exists first
		if ( $where == 'url' || file_exists( $template_path ) ) {
			include $template_path;
			break;
		}
	}
	$template = ob_get_clean();

	// return template content
	return $template;
}

function pmpro_getLevelCost( &$level, $tags = true, $short = false ) {
	// initial payment
	if ( ! $short ) {
		$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 {
			$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' ) {
				$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 {
				$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 ) {
			$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 ) {
						$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 {
						$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 ) {
						$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 {
						$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' ) {
					$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 {
					$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 {
				$r .= ' ' . sprintf( __( 'After your initial payment, your first %d payments are Free.', 'paid-memberships-pro' ), $level->trial_limit );
			}
		} else {
			if ( $level->trial_limit == '1' ) {
				$r .= ' ' . sprintf( __( 'After your initial payment, your first payment will cost %s.', 'paid-memberships-pro' ), pmpro_formatPrice( $level->trial_amount ) );
			} else {
				$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 = pmpro_getOption( 'tax_state' );
	$tax_rate = pmpro_getOption( 'tax_rate' );

	if ( $tax_state && $tax_rate && ! pmpro_isLevelFree( $level ) ) {
		$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 ) {
		$r = sprintf( __( 'The price for membership is <strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $initpmt ) );
	} else {
		$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' ) {
							$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 {
							$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 ) {
						$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' ) {
							$billtextparts[] = sprintf( __( '<strong>%1$s every %2$s</strong>', 'paid-memberships-pro' ), pmpro_formatPrice( $curtotal ), pmpro_translate_billing_period( $curperiod ) );
						} else {
							$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.', 'mmpu' );
		} else {
			$r .= sprintf( __( 'Trial pricing has been applied to the first %d payments.', 'mmpu' ), $trialperiods );
		}
	}

	// taxes part
	$tax_state = pmpro_getOption( 'tax_state' );
	$tax_rate = pmpro_getOption( 'tax_rate' );

	if ( $tax_state && $tax_rate && ! pmpro_areLevelsFree( $levels ) ) {
		$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 ) {

	if ( $level->expiration_number ) {
		$expiration_text = sprintf( __( '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( __( '%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;
}

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

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

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

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

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 ) {
		$r = false;
	} else {
		// get last order
		$order = new MemberOrder();
		$order->getLastMemberOrder( $user_id, $order_status );

		// get current membership level
		$level = pmpro_getMembershipLevelForUser( $user_id );

		if ( ! empty( $order ) && ! empty( $order->id ) && ! empty( $level ) && ! empty( $level->id ) && ! empty( $level->cycle_number ) ) {
			// next payment date
			$nextdate = strtotime( '+' . $level->cycle_number . ' ' . $level->cycle_period, $order->getTimestamp() );

			$r = $nextdate;
		} else {
			// no order or level found, or level was not recurring
			$r = false;
		}
	}

	/**
	 * Filter the next payment date.
	 *
	 * @since 1.8.5
	 *
	 * @param mixed $r false or the next payment date timestamp
	 * @param int $user_id The user id to get the next payment date for
	 * @param string $order_status Status or array of statuses to find the last order based on.
	 */
	$r = apply_filters( 'pmpro_next_payment', $r, $user_id, $order_status );

	// return in desired format
	if ( $r === false ) {
		return false;               // always return false when no date found
	} elseif ( $format == 'timestamp' ) {
		return $r;
	} elseif ( $format == 'date_format' ) {
		return date_i18n( get_option( 'date_format' ), $r );
	} else {
		return date_i18n( $format, $r );  // assume a PHP date format
	}
}

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

function pmpro_showRequiresMembershipMessage() {
	// TODO $current_user $post_membership_levels_names are undefined variables
	// get the correct message
	if ( is_feed() ) {
		$content = pmpro_getOption( 'rsstext' );
		$content = str_replace( '!!levels!!', implode( ', ', $post_membership_levels_names ), $content );
	} elseif ( $current_user->ID ) {
		// not a member
		$content = pmpro_getOption( 'nonmembertext' );
		$content = str_replace( '!!levels!!', implode( ', ', $post_membership_levels_names ), $content );
	} else {
		// not logged in!
		$content = pmpro_getOption( '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)
 * 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 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 ) ) {
			$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=$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;
}

/**
 * Wrapper for pmpro_changeMembershipLevel to cancel one level.
 *
 * @since 1.8.11
 */
function pmpro_cancelMembershipLevel( $cancel_level, $user_id = null, $old_level_status = 'inactive' ) {
	return pmpro_changeMembershipLevel( 0, $user_id, $old_level_status, $cancel_level );
}

/**
 * Create, add, remove or updates the membership level of the given user to the given level.
 *
 * $level may either be the ID or name of the desired membership_level.
 * If $user_id is omitted, the value will be retrieved from $current_user.
 *
 * @param int    $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 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 If set cancel just this one level instead of all active levels (to support Multiple Memberships per User)
 *
 * @return bool|null
 * 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 $wpdb;
	global $current_user, $pmpro_error;

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

	if ( empty( $level ) ) {
		$level = 0;
	} else if ( is_array( $level ) ) {
		// custom level
		if ( empty( $level['membership_id'] ) ) {
			$pmpro_error = __( 'No membership_id specified in pmpro_changeMembershipLevel.', 'paid-memberships-pro' );
			return false;
		}

		$level_obj = pmpro_getLevel( $level['membership_id'] );
		if ( empty( $level_obj ) ) {
			$pmpro_error = __( 'Invalid level.', 'paid-memberships-pro' );
			return false;
		}
		unset( $level_obj );
	} else {
		// just level id
		$level_obj = pmpro_getLevel( $level );
		if ( empty( $level_obj ) ) {
			$pmpro_error = __( 'Invalid level.', 'paid-memberships-pro' );
			return false;
		}
		$level = $level_obj->id;
		unset( $level_obj );
	}

	// if it's a custom level, they're changing
	if ( ! is_array( $level ) ) {
		// are they even changing?
		if ( pmpro_hasMembershipLevel( $level, $user_id ) ) {
			return;
		}
	}

	// get all active membershipships for this user
	$old_levels = pmpro_getMembershipLevelsForUser( $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 ) ) {
		$pmpro_old_user_levels[$user_id] = empty( $old_levels ) ? array() : $old_levels;
	}

		// get level id
	if ( is_array( $level ) ) {
		$level_id = $level['membership_id'];    // custom level
	} else {
		$level_id = $level; // just 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, $old_levels, $cancel_level );

	// deactivate old memberships based on the old_level_status passed in (updates pmpro_memberships_users table)
	$pmpro_deactivate_old_levels = true;
	/**
	 * Filter whether old levels should be deactivated or not. This supports the MMPU addon.
	 * 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', $pmpro_deactivate_old_levels );

	// make sure we deactivate the specified level if it's passed in
	if ( ! empty( $cancel_level ) ) {
		$pmpro_deactivate_old_levels = true;
		$new_old_levels = array();
		foreach ( $old_levels as $key => $old_level ) {
			if ( $old_level->id == $cancel_level ) {
				$new_old_levels[] = $old_levels[ $key ];
				break;
			}
		}
		$old_levels = $new_old_levels;
	}

	if ( $old_levels && $pmpro_deactivate_old_levels ) {
		foreach ( $old_levels as $old_level ) {

			$sql = "UPDATE $wpdb->pmpro_memberships_users SET `status`='$old_level_status', `enddate`='" . esc_sql( current_time( 'mysql' ) ) . "' WHERE `id`=" . esc_sql( $old_level->subscription_id );

			if ( ! $wpdb->query( $sql ) ) {
				$pmpro_error = __( 'Error interacting with database', 'paid-memberships-pro' ) . ': ' . ( $wpdb->last_error ? $wpdb->last_error : 'unavailable' );

				return false;
			}
		}
	}

	// should we cancel their gateway subscriptions?
	if ( ! empty( $cancel_level ) ) {
		$pmpro_cancel_previous_subscriptions = true;    // don't filter cause we're doing just the one

		$other_order_ids = $wpdb->get_col( "SELECT id FROM $wpdb->pmpro_membership_orders WHERE user_id = '" . esc_sql( $user_id ) . "' AND status = 'success' AND membership_id = '" . esc_sql( $cancel_level ) . "' ORDER BY id DESC LIMIT 1" );
	} else {
		$pmpro_cancel_previous_subscriptions = true;
		if ( isset( $_REQUEST['cancel_membership'] ) && $_REQUEST['cancel_membership'] == false ) {
			$pmpro_cancel_previous_subscriptions = false;
		}
		$pmpro_cancel_previous_subscriptions = apply_filters( 'pmpro_cancel_previous_subscriptions', $pmpro_cancel_previous_subscriptions );

		$other_order_ids = $wpdb->get_col(
			"SELECT id, IF(subscription_transaction_id = '', CONCAT('UNIQUE_SUB_ID_', id), subscription_transaction_id) as unique_sub_id
											FROM $wpdb->pmpro_membership_orders
											WHERE user_id = '" . esc_sql( $user_id ) . "'
												AND status = 'success'
											GROUP BY unique_sub_id
											ORDER BY id DESC"
		);
	}

	/**
	 * Filter the other/old order ids in case we want to exclude some.
	 * NOTE: As of version 2.0.3, includes/filters.php has code to
	 * ignore the order for the current checkout.
	 */
	$other_order_ids = apply_filters( 'pmpro_other_order_ids_to_cancel', $other_order_ids );

	// cancel any other subscriptions they have (updates pmpro_membership_orders table)
	if ( $pmpro_cancel_previous_subscriptions && ! empty( $other_order_ids ) ) {
		foreach ( $other_order_ids as $order_id ) {
			$c_order = new MemberOrder( $order_id );
			$c_order->cancel();

			if ( ! empty( $c_order->error ) ) {
				$pmpro_error = $c_order->error;
			}

			if( $old_level_status == 'error' ) {
				$c_order->updateStatus("error");
			}
		}
	}

	// 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 ) ) {
			$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.
				)
			);
		}
	}

	// remove cached level
	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 user data and call action
	pmpro_set_current_user();

	/**
	 * 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, $cancel_level );
	return true;
}

/**
 * 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 occured, 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 occured. 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 $cat->name; ?></label>
				<?php pmpro_listCategories( $cat->term_id, $level_categories ); ?>
			</div>
			<?php
		}
	}
}

/**
 * pmpro_toggleMembershipCategory() creates or deletes a linking entry between the membership level and post category tables.
 *
 * @param $level may either be the ID or name of the desired membership_level.
 * @param $category must be a valid post category ID.
 * @param $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 = '$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 ('$level','$category')";
		$wpdb->query( $sql );
		if ( $wpdb->last_error ) {
			return $wpdb->last_error;
		}
	} else {
		$sql = "DELETE FROM {$wpdb->pmpro_memberships_categories} WHERE `membership_id` = '$level' AND `category_id` = '$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 $level is a valid membership level ID or name
 * @param $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 all existing links...
	$sqlQuery = "DELETE FROM $wpdb->pmpro_memberships_categories WHERE `membership_id` = '" . esc_sql( $level ) . "'";
	$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 $level_id is a valid membership level ID
 *
 * @return bool
 * Return values:
 *		Success returns boolean true.
 *		Failure returns boolean false.
 */
function pmpro_getMembershipCategories( $level_id ) {
	$level_id = intval( $level_id );

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

	return $categories;
}


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

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

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
function pmpro_getPaginationString( $page = 1, $totalitems = 0, $limit = 15, $adjacents = 1, $targetpage = '/', $pagestring = '&pn=' ) {
	// defaults
	if ( ! $adjacents ) {
		$adjacents = 1;
	}
	if ( ! $limit ) {
		$limit = 15;
	}
	if ( ! $page ) {
		$page = 1;
	}
	if ( ! $targetpage ) {
		$targetpage = '/';
	}

	// 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

	/*
		Now we apply our rules and draw the pagination object.
		We're actually saving the code to a variable in case we want to draw it more than once.
	*/
	$pagination = '';
	if ( $lastpage > 1 ) {
		$pagination .= '<span class="pmpro_pagination"';
		if ( ! empty( $margin ) || ! empty( $padding ) ) {
			$pagination .= ' style="';
			if ( $margin ) {
				$pagination .= "margin: $margin;";
			}
			if ( $padding ) {
				$pagination .= "padding: $padding;";
			}
			$pagination .= '"';
		}
		$pagination .= '>';

		// previous button
		if ( $page > 1 ) {
			$pagination .= "<a href=\"$targetpage$pagestring$prev\">&laquo; prev</a>";
		} else {
			$pagination .= '<span class="disabled">&laquo; prev</span>';
		}

		// pages
		if ( $lastpage < 7 + ( $adjacents * 2 ) ) {
			for ( $counter = 1; $counter <= $lastpage; $counter++ ) {
				if ( $counter == $page ) {
					$pagination .= "<span class=\"current\">$counter</span>";
				} else {
					$pagination .= '<a href="' . $targetpage . $pagestring . $counter . "\">$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=\"current\">$counter</span>";
					} else {
						$pagination .= '<a href="' . $targetpage . $pagestring . $counter . "\">$counter</a>";
					}
				}
				$pagination .= '...';
				$pagination .= '<a href="' . $targetpage . $pagestring . $lpm1 . "\">$lpm1</a>";
				$pagination .= '<a href="' . $targetpage . $pagestring . $lastpage . "\">$lastpage</a>";
			} // in middle; hide some front and some back
			elseif ( $lastpage - ( $adjacents * 2 ) > $page && $page > ( $adjacents * 2 ) ) {
				$pagination .= '<a href="' . $targetpage . $pagestring . '1">1</a>';
				$pagination .= '<a href="' . $targetpage . $pagestring . '2">2</a>';
				$pagination .= '...';
				for ( $counter = $page - $adjacents; $counter <= $page + $adjacents; $counter++ ) {
					if ( $counter == $page ) {
						$pagination .= "<span class=\"current\">$counter</span>";
					} else {
						$pagination .= '<a href="' . $targetpage . $pagestring . $counter . "\">$counter</a>";
					}
				}
				$pagination .= '...';
				$pagination .= '<a href="' . $targetpage . $pagestring . $lpm1 . "\">$lpm1</a>";
				$pagination .= '<a href="' . $targetpage . $pagestring . $lastpage . "\">$lastpage</a>";
			} // close to end; only hide early pages
			else {
				$pagination .= '<a href="' . $targetpage . $pagestring . '1">1</a>';
				$pagination .= '<a href="' . $targetpage . $pagestring . '2">2</a>';
				$pagination .= '...';
				for ( $counter = $lastpage - ( 1 + ( $adjacents * 3 ) ); $counter <= $lastpage; $counter++ ) {
					if ( $counter == $page ) {
						$pagination .= "<span class=\"current\">$counter</span>";
					} else {
						$pagination .= '<a href="' . $targetpage . $pagestring . $counter . "\">$counter</a>";
					}
				}
			}
		}

		// next button
		if ( $page < $counter - 1 ) {
			$pagination .= '<a href="' . $targetpage . $pagestring . $next . '">next &raquo;</a>';
		} else {
			$pagination .= '<span class="disabled">next &raquo;</span>';
		}
		$pagination .= "</span>\n";
	}

	return $pagination;

}

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 (double) $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++;

	while ( empty( $code ) ) {
		$scramble = md5( AUTH_KEY . microtime() . $seed . SECURE_AUTH_KEY . $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;

	$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', $dbcode->starts ) );
		$dbcode->expires = strtotime( date_i18n( 'm/d/Y', $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 ) {
			$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 ) {
			$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' );
			}
		}
	}

	// 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 (" . esc_sql( $level_id ) . ") LIMIT 1" );

			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( "'", '"' ) ) {
	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
 *
 * @param string $separator The separator used between action links.
 */
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 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 ) {
	global $current_user;

	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_message_pre = '<div class="' . pmpro_get_element_class( 'pmpro_content_message' ) . '">';
	$pmpro_content_message_post = '</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( $_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() ) {
		$newcontent = apply_filters( 'pmpro_rss_text_filter', stripslashes( pmpro_getOption( 'rsstext' ) ) );
		$content .= $pmpro_content_message_pre . str_replace( $sr_search, $sr_replace, $newcontent ) . $pmpro_content_message_post;
	} elseif ( $current_user->ID ) {
		//not a member
		$newcontent = apply_filters( 'pmpro_non_member_text_filter', stripslashes( pmpro_getOption( 'nonmembertext' ) ) );
		$content .= $pmpro_content_message_pre . str_replace( $sr_search, $sr_replace, $newcontent ) . $pmpro_content_message_post;
	} else {
		//not logged in!
		$newcontent = apply_filters( 'pmpro_not_logged_in_text_filter', stripslashes( pmpro_getOption( 'notloggedintext' ) ) );
		$content .= $pmpro_content_message_pre . str_replace( $sr_search, $sr_replace, $newcontent ) . $pmpro_content_message_post;
	}

	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 actvie 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 ) {
	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 $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,
				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" . ( $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 $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.
 *
 * @param bool $include_hidden Include levels marked as hidden/inactive.
 * @param bool $use_cache      If false, use $pmpro_levels global. If true use other caches.
 * @param bool $force          Resets the static var caches as well.
 */
function pmpro_getAllLevels( $include_hidden = false, $use_cache = false, $force = false ) {
	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;
	}

	// just use the $pmpro_levels global
	if ( ! empty( $pmpro_levels ) && ! $use_cache ) {
		return $pmpro_levels;
	}

	// If use_cache is true check if we have something in a static var.
	if ( $include_hidden && isset( $pmpro_all_levels ) ) {
		return $pmpro_all_levels;
	}
	if ( ! $include_hidden && isset( $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 and place into $pmpro_level global.
 * 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.
 */
function pmpro_getLevelAtCheckout( $level_id = null, $discount_code = null ) {
	global $pmpro_level, $wpdb, $post;

	// reset pmpro_level
	$pmpro_level = null;

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

	// no level, check for a default level in the custom fields for this post
	if ( empty( $level_id ) && ! empty( $post ) ) {

		$level_id = get_post_meta( $post->ID, 'pmpro_default_level', true );

		// if still empty, let's try get first available 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 ) );

			// Bail back to levels page if level ID is empty or less than 1.
			if ( empty( $level_id ) || $level_id < 1 || ! is_int( $level_id ) ) {
				return;
			}
		}
	}

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

	// what level are they purchasing? (discount code passed)
	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
		global $pmpro_checkout_level_ids; // Set by MMPU.
		if ( isset( $pmpro_checkout_level_ids ) ) {
			$code_check = pmpro_checkDiscountCode( $discount_code, $pmpro_checkout_level_ids, true );
		} else {
			$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          = 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' );
		}
	}

	// what level are they purchasing? (no discount code)
	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 = pmpro_getOption( '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 );

		if( ! empty( $level ) ) {

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

			// replace vars
			$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 = __( 'Sign Up Now', 'paid-memberships-pro' );
	}

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

	if ( ! empty( $level_id ) ) {
		$r = '<a href="' . pmpro_url( 'checkout', '?level=' . $level_id ) . '" class="' . $classes . '">' . $button_text . '</a>';
	} else {
		$r = '<a href="' . pmpro_url( 'checkout' ) . '" class="' . $classes . '">' . $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(" . esc_sql( $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 a PMPro message set via pmpro_setMessage
 *
 * @since 1.8.5
 */
function pmpro_showMessage() {
	global $pmpro_msg, $pmpro_msgt;

	if ( ! empty( $pmpro_msg ) ) {
		?>
		<div class="<?php echo pmpro_get_element_class( 'pmpro_msg ' . $pmpro_msgt, $pmpro_msgt ); ?>">
			<p><?php echo $pmpro_msg; ?></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_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_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' ) {
	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 ] );
		}
	}

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

	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             = pmpro_getOption( 'gateway' );
		$gateway_environment = pmpro_getOption( 'gateway_environment' );
		if ( $gateway == 'authorizenet' ) {
			if ( $gateway_environment && pmpro_getOption( 'loginname' ) && pmpro_getOption( 'transactionkey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'paypal' || $gateway == 'paypalexpress' ) {
			if ( $gateway_environment && pmpro_getOption( 'gateway_email' ) && pmpro_getOption( 'apiusername' ) && pmpro_getOption( 'apipassword' ) && pmpro_getOption( 'apisignature' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'paypalstandard' ) {
			if ( $gateway_environment && pmpro_getOption( 'gateway_email' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'payflowpro' ) {
			if ( pmpro_getOption( 'payflow_partner' ) && pmpro_getOption( 'payflow_vendor' ) && pmpro_getOption( 'payflow_user' ) && pmpro_getOption( 'payflow_pwd' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'stripe' ) {
			if ( $gateway_environment && pmpro_getOption( 'stripe_secretkey' ) && pmpro_getOption( 'stripe_publishablekey' ) ) {
				// Using legacy keys.
				$pmpro_gateway_ready = true;
			} elseif ( $gateway_environment && pmpro_getOption( $gateway_environment . '_stripe_connect_secretkey' ) && pmpro_getOption( $gateway_environment . '_stripe_connect_publishablekey' ) ) {
				// Using connect.
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'braintree' ) {
			if ( $gateway_environment && pmpro_getOption( 'braintree_merchantid' ) && pmpro_getOption( 'braintree_publickey' ) && pmpro_getOption( 'braintree_privatekey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'twocheckout' ) {
			if ( $gateway_environment && pmpro_getOption( 'twocheckout_apiusername' ) && pmpro_getOption( 'twocheckout_apipassword' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'cybersource' ) {
			if ( $gateway_environment && pmpro_getOption( 'cybersource_merchantid' ) && pmpro_getOption( '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 Invoice Price Data with Parts
 *
 * @param object $pmpro_invoice 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_invoice, $format = 'array' ) {
	$pmpro_price_parts = array();

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

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

	if ( ! empty( $pmpro_invoice->couponamount ) ) {
		// We don't even use this but it is in the database so it could be shown here.
		$pmpro_price_parts['couponamount'] = array(
			'label' => __( 'Coupon', 'paid-memberships-pro' ),
			'value' => pmpro_escape_price( pmpro_formatPrice( $pmpro_invoice->couponamount ) ),
		);
	}

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

	$pmpro_price_parts_with_total = $pmpro_price_parts;

	if ( ! empty( $pmpro_invoice->total ) ) {
		$pmpro_price_parts_with_total['total'] = array(
			'label' => __( 'Total', 'paid-memberships-pro' ),
			'value' => pmpro_escape_price( pmpro_formatPrice( $pmpro_invoice->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_invoice 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_invoice );

	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( (double) $price, $decimals );

	/**
	 * Filter for result of pmpro_round_price.
	 */
	$rounded = apply_filters( 'pmpro_round_price', $rounded );

	return $rounded;
}

/**
 * Rounds price based on currency and returns a string.
 *
 * Does not format price, to do that, call pmpro_formatPrice().
 *
 * @since 2.8
 *
 * @param int|float|string $amount   The amount to get price information for.
 * @param null|string      $currency The currency to use, defaults to current currency.
 *
 * @return string The rounded price as a string.
 */
function pmpro_round_price_as_string( $amount, $currency = null ) {
	$price_info = pmpro_get_price_info( $amount, $currency );

	if ( ! $price_info ) {
		return (string) pmpro_round_price( $amount, $currency );
	}

	return $price_info['amount_string'];
}

/**
 * Get the price information about the provided amount.
 *
 * @since 2.8
 *
 * @param int|float|string $amount   The amount to get price information for.
 * @param null|string      $currency The currency to use, defaults to current currency.
 *
 * @return array The price information about the provided amount.
 */
function pmpro_get_price_info( $amount, $currency = null ) {
	if ( ! is_numeric( $amount ) ) {
		return false;
	}

	$amount = (float) $amount;

	$currency_info = pmpro_get_currency( $currency );

	$price_info = [
		// The amount represented as a float.
		'amount'        => $amount,
		// The flat amount represent (example: 1.99 would be 199).
		'amount_flat'   => 0,
		// The amount as a string.
		'amount_string' => '',
		'parts'         => [
			// The whole number part of the amount (example: 1.99 would be 1).
			'number'         => (int) $amount,
			// The decimal part of the amount (example: 1.99 would be 99, 1.00 would be 0).
			'decimal'        => 0,
			// The decimal part of the amount as a string (example: 1.99 would be 99, 1.00 would be 00).
			'decimal_string' => '',
		],
		// The currency information.
		'currency'      => $currency_info,
	];

	// Enforce integer.
	$currency_info['decimals'] = (int) $currency_info['decimals'];

	$multiplier = 1;

	if ( 0 < $currency_info['decimals'] ) {
		$multiplier = pow( 10, $currency_info['decimals'] );
	}

	// Convert the amount from 100.99 to 10099.
	$price_info['amount_flat'] = $amount * $multiplier;

	// If there were additional unsupported decimal points, round to remove and convert to integer.
	$price_info['amount_flat'] = round( $price_info['amount_flat'] );

	// Get the decimal part of the amount as a whole number.
	$price_info['parts']['decimal'] = (
		$price_info['amount_flat'] - (
			$price_info['parts']['number'] * $multiplier
		)
	);

	// Get the zero-padded decimal amount.
	$price_info['parts']['decimal_string'] = sprintf( '%02d', $price_info['parts']['decimal'] );

	// Get the amount as a string.
	$price_info['amount_string'] = sprintf( '%s.%s', $price_info['parts']['number'], $price_info['parts']['decimal_string'] );

	return $price_info;
}

/**
 * Cast to floats and pad zeroes after the decimal
 * when editing the price on the edit level page.
 * Only do this for currency with decimals = 2
 * Only do this if using . as the decimal separator.
 * Only pad zeroes to the decimal portion if there is exactly one number
 * after the decimal.
 *
 * @since  2.0.2
 */
function pmpro_filter_price_for_text_field( $price, $currency = null ) {
	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 = $_REQUEST['gateway'];        // gateway passed as param
	} elseif ( ! empty( $_REQUEST['review'] ) ) {
		$gateway = 'paypalexpress';             // if review param assume paypalexpress
	} else {
		$gateway = pmpro_getOption( 'gateway' );  // get from options
	}

	// set valid gateways - the active gateway in the settings and any gateway added through the filter will be allowed
	if ( pmpro_getOption( 'gateway', true ) == 'paypal' ) {
		$valid_gateways = apply_filters( 'pmpro_valid_gateways', array( 'paypal', 'paypalexpress' ) );
	} else {
		$valid_gateways = apply_filters( 'pmpro_valid_gateways', array( pmpro_getOption( 'gateway', true ) ) );
	}

	// 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
			$top_level_pages = array( 'account', 'login' );
			if ( ! in_array( $name, $top_level_pages ) ) {
				$insert['post_parent'] = $pmpro_pages['account'];
			}

			// 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
			pmpro_setOption( $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 $safe 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;
	}
}

/**
  * Return an array of allowed order statuses
  *
  * @since 1.9.3
  */
function pmpro_getOrderStatuses( $force = false ) {
	global $pmpro_order_statuses;

	if ( ! isset( $pmpro_order_statuses ) || $force ) {
		global $wpdb;
		$statuses         = array();
		$default_statuses = array( '', 'success', 'cancelled', 'review', 'token', 'refunded', 'pending', 'error' );
		$used_statuses    = $wpdb->get_col( "SELECT DISTINCT(status) FROM $wpdb->pmpro_membership_orders" );
		$statuses         = array_unique( array_merge( $default_statuses, $used_statuses ) );
		asort( $statuses );
		$statuses = apply_filters( 'pmpro_order_statuses', $statuses );
	}

	return $statuses;
}

/**
 * Cleanup the wp_pmpro_memberships_users_table
 * (a) If a user has more than one active row for the same level,
 * the older ones are marked inactive.
 * (b) If any user has active rows for an non-existent level id,
 * those rows are marked as inactive.
 *
 * @since 1.9.4.4
 */
function pmpro_cleanup_memberships_users_table() {
	global $wpdb;

	// fix rows for levels that don't exists
	$sqlQuery = "UPDATE $wpdb->pmpro_memberships_users mu
					LEFT JOIN $wpdb->pmpro_membership_levels l ON mu.membership_id = l.id
				SET mu.status = 'inactive'
				WHERE mu.status = 'active'
					AND l.id IS NULL";
	$wpdb->query( $sqlQuery );

	// fix rows where there is more than one active status for the same user/level
	$sqlQuery = "UPDATE $wpdb->pmpro_memberships_users t1
					INNER JOIN (SELECT mu1.id as id
				FROM $wpdb->pmpro_memberships_users mu1, $wpdb->pmpro_memberships_users mu2
				WHERE mu1.id < mu2.id
					AND mu1.user_id = mu2.user_id
					AND mu1.membership_id = mu2.membership_id
					AND mu1.status = 'active'
					AND mu2.status = 'active'
				GROUP BY mu1.id
				ORDER BY mu1.user_id, mu1.id DESC) t2
				ON t1.id = t2.id
				SET status = 'inactive'";
	$wpdb->query( $sqlQuery );
}

/**
 * Are we on the PMPro checkout page?
 * @since 2.1
 * @return bool True if we are on the checkout page, false otherwise
 */
function pmpro_is_checkout() {
	global $pmpro_pages;

	// Try is_page first.
	if ( ! empty( $pmpro_pages['checkout'] ) ) {
		$is_checkout = is_page( $pmpro_pages['checkout'] );
	} else {
		$is_checkout = false;
	}

	// Page might not be setup yet or a custom page.
	$queried_object = get_queried_object();

	if ( ! $is_checkout &&
		! empty( $queried_object ) &&
		! empty( $queried_object->post_content ) &&
		( has_shortcode( $queried_object->post_content, 'pmpro_checkout' ) ||
			( function_exists( 'has_block' ) &&
				has_block( 'pmpro/checkout-page', $queried_object->post_content )
			)
		)
	) {
		$is_checkout = true;
	}

	/**
	 * Filter for pmpro_is_checkout return value.
	 * @since 2.1
	 * @param bool $is_checkout true if we are on the checkout page, false otherwise
	 */
	$is_checkout = apply_filters( 'pmpro_is_checkout', $is_checkout );

	return $is_checkout;
}

/**
 * Are we showing discount codes at checkout?
 */
function pmpro_show_discount_code() {
	global $wpdb;
	static $show;

	// check DB if we haven't yet
	if ( !isset( $show ) ) {
		if ( $wpdb->get_var( "SELECT id FROM $wpdb->pmpro_discount_codes LIMIT 1" ) ) {
			$show = true;
		} else {
			$show = false;
		}
	}

	$show = apply_filters( "pmpro_show_discount_code", $show );

	return $show;
}

/**
 * Check if the checkout form was submitted.
 * Accounts for image buttons/etc.
 * @since 2.1
 * @return bool True if the form was submitted, else false.
 */
 function pmpro_was_checkout_form_submitted() {
	 // Default to false.
	 $submit = false;

	 // Basic check for a field called submit-checkout.
	 if ( isset( $_REQUEST['submit-checkout'] ) ) {
	 	$submit = true;
	 }

	 // _x stuff in case they clicked on the image button with their mouse
	 if ( empty( $submit ) && isset( $_REQUEST['submit-checkout_x'] ) ) {
	 	$submit = true;
	 }

	 return $submit;
 }

 /**
  * Build the order object used at checkout.
  * @since 2.1
  * @return mixed $order Order object.
  */
 function pmpro_build_order_for_checkout() {
	global $post, $gateway, $wpdb, $besecure, $discount_code, $discount_code_id, $pmpro_level, $pmpro_levels, $pmpro_msg, $pmpro_msgt, $pmpro_review, $skip_account_fields, $pmpro_paypal_token, $pmpro_show_discount_code, $pmpro_error_fields, $pmpro_required_billing_fields, $pmpro_required_user_fields, $wp_version, $current_user, $pmpro_requirebilling, $tospage, $username, $password, $password2, $bfirstname, $blastname, $baddress1, $baddress2, $bcity, $bstate, $bzipcode, $bcountry, $bphone, $bemail, $bconfirmemail, $CardType, $AccountNumber, $ExpirationMonth, $ExpirationYear, $pmpro_states, $recaptcha, $recaptcha_privatekey, $CVV;

	$morder                   = new MemberOrder();
	$morder->membership_id    = $pmpro_level->id;
	$morder->membership_name  = $pmpro_level->name;
	$morder->discount_code    = $discount_code;
	$morder->InitialPayment   = pmpro_round_price( $pmpro_level->initial_payment );
	$morder->PaymentAmount    = pmpro_round_price( $pmpro_level->billing_amount );
	$morder->ProfileStartDate = date_i18n( "Y-m-d\TH:i:s", current_time( "timestamp" ) );
	$morder->BillingPeriod    = $pmpro_level->cycle_period;
	$morder->BillingFrequency = $pmpro_level->cycle_number;
	if ( $pmpro_level->billing_limit ) {
		$morder->TotalBillingCycles = $pmpro_level->billing_limit;
	}
	if ( pmpro_isLevelTrial( $pmpro_level ) ) {
		$morder->TrialBillingPeriod    = $pmpro_level->cycle_period;
		$morder->TrialBillingFrequency = $pmpro_level->cycle_number;
		$morder->TrialBillingCycles    = $pmpro_level->trial_limit;
		$morder->TrialAmount           = pmpro_round_price( $pmpro_level->trial_amount );
	}

	// Credit card values.
	$morder->cardtype              = $CardType;
	$morder->accountnumber         = $AccountNumber;
	$morder->expirationmonth       = $ExpirationMonth;
	$morder->expirationyear        = $ExpirationYear;
	$morder->ExpirationDate        = $ExpirationMonth . $ExpirationYear;
	$morder->ExpirationDate_YdashM = $ExpirationYear . "-" . $ExpirationMonth;
	$morder->CVV2                  = $CVV;

	// Not saving email in order table, but the sites need it.
	$morder->Email = $bemail;

	// Save the user ID if logged in.
	if ( $current_user->ID ) {
		$morder->user_id = $current_user->ID;
	}

	// Sometimes we need these split up.
	$morder->FirstName = $bfirstname;
	$morder->LastName  = $blastname;
	$morder->Address1  = $baddress1;
	$morder->Address2  = $baddress2;

	// Set other values.
	$morder->billing          = new stdClass();
	$morder->billing->name    = $bfirstname . " " . $blastname;
	$morder->billing->street  = trim( $baddress1 . " " . $baddress2 );
	$morder->billing->city    = $bcity;
	$morder->billing->state   = $bstate;
	$morder->billing->country = $bcountry;
	$morder->billing->zip     = $bzipcode;
	$morder->billing->phone   = $bphone;
	$morder->gateway = $gateway;
	$morder->setGateway();

	// Set up level var.
	$morder->getMembershipLevelAtCheckout();

	// Set tax.
	$initial_tax = $morder->getTaxForPrice( $morder->InitialPayment );
	$recurring_tax = $morder->getTaxForPrice( $morder->PaymentAmount );

	// Set amounts.
	$morder->initial_amount = pmpro_round_price((float)$morder->InitialPayment + (float)$initial_tax);
	$morder->subscription_amount = pmpro_round_price((float)$morder->PaymentAmount + (float)$recurring_tax);

	// Filter for order, since v1.8
	$morder = apply_filters( 'pmpro_checkout_order', $morder );

	return $morder;
}

/**
 * Compare a plugin's version to a given version number.
 *
 * @param  string $plugin_file plugin to compare.
 * @param  string $comparison type of comparison to perform.
 * @param  string $version version to compare to.
 * @return bool
 */
function pmpro_check_plugin_version( $plugin_file, $comparison, $version ) {
	// Make sure data to check is in a good format.
	if ( empty( $plugin_file ) || empty( $comparison ) || ! isset( $version ) ) {
		return false;
	}

	// Get plugin data.
	$full_plugin_file_path = WP_PLUGIN_DIR . '/' . $plugin_file;
	if ( is_file( $full_plugin_file_path ) ) {
		$plugin_data = get_plugin_data( $full_plugin_file_path, false, true );
	}

	// Return false if there is no plugin data.
	if ( empty( $plugin_data ) || empty( $plugin_data['Version'] ) ) {
		return false;
	}

	// Check version.
	if ( version_compare( $plugin_data['Version'], $version, $comparison ) ) {
		return true;
	} else {
		return false;
	}
}

/**
 * Compare two integers using parameters similar to the version_compare function.
 * This allows us to pass in a comparison character via the notification rules
 * and get a true/false result.
 * @param int $a First integer to compare.
 * @param int $b Second integer to compare.
 * @param string $operator Operator to use, e.g. >, <, >=, <=, =.
 * @return bool true or false based on the operator passed in. Returns null for invalid operators.
 */
function pmpro_int_compare( $a, $b, $operator ) {
	switch ( $operator ) {
		case '>':
			$r = (int)$a > (int)$b;
			break;
		case '<':
			$r = (int)$a < (int)$b;
			break;
		case '>=':
			$r = (int)$a >= (int)$b;
			break;
		case '<=':
			$r = (int)$a <= (int)$b;
			break;
		case '=':
		case '==':
			$r = (int)$a == (int)$b;
			break;
		default:
			$r = null;
	}

	return $r;
}

/**
 * Wrapper for $wpdb to insert or replace
 * based on the value of the primary key field.
 * Using this since using REPLACE on some setups
 * results in unexpected behavior.
 *
 * @since 2.4
 */
function pmpro_insert_or_replace( $table, $data, $format, $primary_key = 'id' ) {
	global $wpdb;

	if ( empty( $data[$primary_key] ) ) {
		// Insert. Remove keys first.
		$index = array_search( $primary_key, array_keys( $data ) );
		if ( $index !== false ) {
			unset( $data[$primary_key] );
			unset( $format[$index] );
		}
		return $wpdb->insert( $table, $data, $format );
	} else {
		// Replace.
		$replaced = $wpdb->replace( $table, $data, $format );
	}
}

/**
 * Checks if a webhook is running
 * @since 2.5
 * @param string $gateway If passed in, requires that specific gateway.
 * @param bool $set Set to true to set the constant and fire the action hook.
 * @return bool True or false if a PMPro webhook set the constant or not.
 */
function pmpro_doing_webhook( $gateway = null, $set = false ){
	// If second param is set, set things up.
	if ( ! empty( $set ) ) {
		define( 'PMPRO_DOING_WEBHOOK', $gateway );
		do_action( 'pmpro_doing_webhook', $gateway );
		return true;
	}

	// Otherwise, check if we were already set up.
	if( defined( 'PMPRO_DOING_WEBHOOK' ) && !empty ( PMPRO_DOING_WEBHOOK ) ){
		if( $gateway !== null ){
			if( PMPRO_DOING_WEBHOOK == $gateway ){
				return true;
			} else {
				return false;
			}
		} else {
			return true;
		}
	} else {
		return false;
	}

}

/**
 * Called once a webhook has been run but was not handled.
 *
 * @return void
 *
 * @since 2.8
 */
function pmpro_unhandled_webhook(){
	/**
	 * Allow hooking into after a webhook has been run but was not handled.
	 *
	 * @since 2.8
	 *
	 * @param string $gateway The gateway the webhook was not handled for.
	 */
	do_action( 'pmpro_unhandled_webhook', PMPRO_DOING_WEBHOOK );
}

/**
 * Sanitizing strings using wp_kses and allowing style tags.
 *
 * @param string $original_string  The string to sanitize.
 * @param string $context          The sanitization context.
 *
 * @return string The sanitized string.
 *
 * @since 2.6.1
 */
function pmpro_kses( $original_string, $context = 'email' ) {
	$context = 'pmpro_' . $context;

	$sanitized_string = $original_string;

	if ( 'pmpro_email' === $context ) {
		// Always remove script tags and their contents.
		$sanitized_string = preg_replace( '@<script[^>]*?>.*?</script>@si', '', $sanitized_string );
	}

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

	/**
	 * Allow overriding the normal pmpro_kses functionality for a context.
	 *
	 * @param string $sanitized_string The sanitized string.
	 * @param string $original_string  The original string.
	 * @param string $context          The sanitization context.
	 *
	 * @since 2.6.2
	 */
	return apply_filters( 'pmpro_kses', $sanitized_string, $original_string, $context );
}

/**
 * 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 publically.
 *
 * Useful for preparing to change method visibility from public to private.
 *
 * @param string $deprecation_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( $backtrace[1]['function'], $deprecated_notice_version );
		return true;
	}
	return false;
}

/**
 * Send a 200 HTTP reponse 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 $date date A valid date value.
 * @return string The date in ISO-8601 format.
 */
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( ',', $_SERVER[ $header ] );
			$client_ip     = trim( $address_chain[0] );

			break;
		}
	}

	if ( ! $client_ip ) {
		return false;
	}

	// Sanitize the IP
	$client_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $client_ip );

	return $client_ip;
}

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

}