<?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';
}
pmpro_setDBTables();

// from: http://stackoverflow.com/questions/5266945/wordpress-how-detect-if-current-page-is-the-login-page/5892694#5892694
function pmpro_is_login_page() {
	return ( in_array( $GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ) ) || is_page( 'login' ) );
}

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

// scraping - override n 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' ) {
	// 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 );
}

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 = '" . $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;

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

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

	return $url;
}

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

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

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

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

function pmpro_isLevelExpiring( &$level ) {
	if ( ! empty( $level ) && ( ! empty( $level->expiration_number ) && $level->expiration_number > 0 ) || ! empty( $level->enddate ) ) {
		return true;
	} else {
		return false;
	}
}

/**
 * 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 == '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 ( $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 );

	// user specified a custom template path, so it has priority.
	if ( ! empty( $user_templates ) ) {
		$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 {
		$r = sprintf( __( '<strong>%s</strong> now', 'paid-memberships-pro' ), pmpro_formatPrice( $level->initial_payment ) );
	}

	// recurring part
	if ( $level->billing_amount != '0.00' ) {
		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 ( $level->trial_amount == '0.00' ) {
			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 ( $curlevel->billing_amount != '0.00' ) {
			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 );
	$expiration_text = apply_filters( 'pmpro_level_expiration_text', $expiration_text, $levels ); // Backwards compatible
	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 );
}

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 ) ) {
			// last payment date
			$lastdate = date_i18n( 'Y-m-d', $order->timestamp );

			// next payment date
			$nextdate = $wpdb->get_var( "SELECT UNIX_TIMESTAMP('" . $lastdate . "' + INTERVAL " . $level->cycle_number . ' ' . $level->cycle_period . ')' );

			$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 ) {
							$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 ) ) { // if a level name was passed
						$return = $found_level;
					}
				}
			}
		}
	}

	$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 values:
 *      Success returns boolean true.
 *      Failure returns boolean false.
 */
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 );

	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 ) ) {
			$pmpro_error = __( 'not changing?', 'paid-memberships-pro' );
			return false; // not changing
		}
	}

	// get all active membershipships for this user
	$old_levels = pmpro_getMembershipLevelsForUser( $user_id );

		// 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`='" . current_time( 'mysql' ) . "' WHERE `id`=" . $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 = '" . $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 = '" . $user_id . "' 
												AND status = 'success' 
											GROUP BY unique_sub_id  
											ORDER BY id DESC"
		);
	}

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

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

	// remove cached level
	global $all_membership_levels;
	unset( $all_membership_levels[ $user_id ] );

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

/**
 * 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 ) {
			$name = 'membershipcategory_' . $cat->term_id;
			if ( ! empty( $level_categories ) ) {
				$checked = checked( in_array( $cat->term_id, $level_categories ), true, false );
			} else {
				$checked = '';
			}
			echo "<ul><li class=membershipcategory><input type=checkbox name={$name} id={$name} value=yes {$checked}><label for={$name}>{$cat->name}</label>";
			pmpro_listCategories( $cat->term_id, $level_categories );
			echo '</li></ul>';
		}
	}
}

/*
 pmpro_toggleMembershipCategory() creates or deletes a linking entry between the membership level and post category tables.
 *
 * $level may either be the ID or name of the desired membership_level.
 * $category must be a valid post category ID.
 *
 * 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.
*
* $level is a valid membership level ID or name
* $categories is an array of post category IDs
*
* 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
*
* $level_id is a valid membership level ID
*
* 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 = '" . $level_id . "'"
	);

	return $categories;
}


function pmpro_isAdmin( $user_id = null ) {
	global $current_user, $wpdb;
	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 ( $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, $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 .= '<div 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 .= "</div>\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((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;
}

function pmpro_generateUsername( $firstname = '', $lastname = '', $email = '' ) {
	global $wpdb;

	// try first initial + last name, firstname, lastname
	$firstname = preg_replace( '/[^A-Za-z]/', '', $firstname );
	$lastname = preg_replace( '/[^A-Za-z]/', '', $lastname );
	if ( $firstname && $lastname ) {
		$username = substr( $firstname, 0, 1 ) . $lastname;
	} elseif ( $firstname ) {
		$username = $firstname;
	} elseif ( $lastname ) {
		$username = $lastname;
	}

	// is it taken?
	$taken = $wpdb->get_var( "SELECT user_login FROM $wpdb->users WHERE user_login = '" . esc_sql( $username ) . "' LIMIT 1" );

	if ( ! $taken ) {
		return $username;
	}

	// try the beginning of the email address
	$emailparts = explode( '@', $email );
	if ( is_array( $emailparts ) ) {
		$email = preg_replace( '/[^A-Za-z]/', '', $emailparts[0] );
	}

	if ( ! empty( $email ) ) {
		$username = $email;
	}

	// is this taken? if not, add numbers until it works
	$taken = true;
	$count = 0;
	while ( $taken ) {
		// add a # to the end
		if ( $count ) {
			$username = preg_replace( '/[0-9]/', '', $username ) . $count;
		}

		// taken?
		$taken = $wpdb->get_var( "SELECT user_login FROM $wpdb->users WHERE user_login = '" . esc_sql( $username ) . "' LIMIT 1" );

		// increment the number
		$count++;
	}

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

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

	while ( empty( $code ) ) {
		$scramble = md5( AUTH_KEY . current_time( 'timestamp' ) . $seed . SECURE_AUTH_KEY );
		$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(starts) as starts, UNIX_TIMESTAMP(expires) 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 00:00: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 = '" . $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 = '" . $dbcode->id . "' AND cl.level_id IN (" . $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 $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' );
	}

	return implode( ', ', $array ) . ' ' . $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;
}

/*
 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 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.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(startdate) as startdate,
															UNIX_TIMESTAMP(enddate) 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;
		}

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

	$levels = $wpdb->get_results(
		"SELECT
								l.id AS ID,
								l.id as id,
								mu.id as subscription_id,
								l.name,
								l.description,
								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(startdate) as startdate,
								UNIX_TIMESTAMP(enddate) 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" )
	);
	/**
	 * 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 omitted, the value will be retrieved from $current_user.
 *
 * Return values:
 *      Success returns the level object.
 *      Failure returns false.
 *
 * @param  int $user_id User ID to check for
 * @param  int $level_id Level ID to check for.
 */
function pmpro_getSpecificMembershipLevelForUser( $user_id = null, $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
 *
 * $level may be the level id or name
 *
 * 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 = '" . $level_id . "' LIMIT 1" );
			return $pmpro_levels[ $level_id ];
		}
	} 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;
		return $pmpro_levels[ $level_id ];
	}
}

/*
	Function to populate pmpro_levels with all levels. We query the DB every time just to be sure we have the latest.
	This should be called if you want to be sure you get all levels as $pmpro_levels may only have a subset of levels.
*/
function pmpro_getAllLevels( $include_hidden = false, $force = false ) {
	global $pmpro_levels, $wpdb;

	// just use what's cached (doesn't take into account include_hidden setting)
	if ( ! empty( $pmpro_levels ) && ! $force ) {
		return $pmpro_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 ) {
		$pmpro_levels[ $raw_level->id ] = $raw_level;
	}

	return $pmpro_levels;
}

/**
 * Get level at checkout and place into $pmpro_level global.
 */
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 );
	}

	// 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 = '" . $discount_code . "' LIMIT 1" );

		// check code
		$code_check = pmpro_checkDiscountCode( $discount_code, $level_id, true );
		if ( $code_check[0] != false ) {
			$sqlQuery    = "SELECT l.id, cl.*, l.name, l.description, l.allow_signups 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 = '" . $discount_code . "' AND cl.level_id = '" . $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 = '" . $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;
}

function pmpro_getCheckoutButton( $level_id, $button_text = null, $classes = null ) {
	if ( empty( $button_text ) ) {
		$button_text = __( 'Sign Up for !!name!! Now', 'paid-memberships-pro' );
	}

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

	if ( empty( $level_id ) ) {
		$r = __( 'Please specify a level id.', 'paid-memberships-pro' );
	} else {
		// get level
		$level = pmpro_getLevel( $level_id );

		if ( empty( $level ) ) {
			$r = sprintf( __( 'Level #%s not found.', 'paid-memberships-pro' ), $level_id );
		} else {
			// replace vars
			$replacements = array(
				'!!id!!' => $level->id,
				'!!name!!' => $level->name,
				'!!description!!' => $level->description,
				'!!confirmation!!' => $level->confirmation,
				'!!initial_payment!!' => $level->initial_payment,
				'!!billing_amount!!' => $level->billing_amount,
				'!!cycle_number!!' => $level->cycle_number,
				'!!cycle_period!!' => $level->cycle_period,
				'!!billing_limit!!' => $level->billing_limit,
				'!!trial_amount!!' => $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 );
		}

		// button text
		$r = '<a href="' . pmpro_url( 'checkout', '?level=' . $level_id ) . '" 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;
}

/*
	Get a member's start date... either in general or for a specific level_id.
*/
if ( ! function_exists( 'pmpro_getMemberStartdate' ) ) {
	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(startdate) 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(startdate) FROM $wpdb->pmpro_memberships_users WHERE status = 'active' AND user_id = '" . $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 ];
	}
}

/*
	How long has this member been a member
*/
if ( ! function_exists( 'pmpro_getMemberDays' ) ) {
	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_msgt; ?>">
			<p><?php echo $pmpro_msg; ?></p>
		</div>
		<?php
	}
}

// used in class definitions for input fields to see if there was an error
function pmpro_getClassForField( $field ) {
	global $pmpro_error_fields, $pmpro_required_billing_fields, $pmpro_required_user_fields;
	$classes = array();

	// error on this field?
	if ( ! empty( $pmpro_error_fields ) && in_array( $field, $pmpro_error_fields ) ) {
		$classes[] = '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( $field, $required_fields ) ) {
		$classes[] = 'pmpro_required';
	}

	$classes = apply_filters( 'pmpro_field_classes', $classes, $field );

	if ( ! empty( $classes ) ) {
		return implode( ' ', $classes );
	} else {
		return '';
	}
}

// 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' );
		if ( $gateway == 'authorizenet' ) {
			if ( pmpro_getOption( 'gateway_environment' ) && pmpro_getOption( 'loginname' ) && pmpro_getOption( 'transactionkey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'paypal' || $gateway == 'paypalexpress' ) {
			if ( pmpro_getOption( '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 ( pmpro_getOption( '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 ( pmpro_getOption( 'gateway_environment' ) && pmpro_getOption( 'stripe_secretkey' ) && pmpro_getOption( 'stripe_publishablekey' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'braintree' ) {
			if ( pmpro_getOption( '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 ( pmpro_getOption( 'gateway_environment' ) && pmpro_getOption( 'twocheckout_apiusername' ) && pmpro_getOption( 'twocheckout_apipassword' ) ) {
				$pmpro_gateway_ready = true;
			} else {
				$pmpro_gateway_ready = false;
			}
		} elseif ( $gateway == 'cybersource' ) {
			if ( pmpro_getOption( '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;
}

/**
 * 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 price formatted with two decimals
	$formatted = number_format( (double) $price, 2 );

	// 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(
			$price,
			( isset( $pmpro_currencies[ $pmpro_currency ]['decimals'] ) ? (int) $pmpro_currencies[ $pmpro_currency ]['decimals'] : 2 ),
			( isset( $pmpro_currencies[ $pmpro_currency ]['decimal_separator'] ) ? $pmpro_currencies[ $pmpro_currency ]['decimal_separator'] : '.' ),
			( isset( $pmpro_currencies[ $pmpro_currency ]['thousands_separator'] ) ? $pmpro_currencies[ $pmpro_currency ]['thousands_separator'] : ',' )
		);

		// which side is the symbol on?
		if ( ! empty( $pmpro_currencies[ $pmpro_currency ]['position'] ) && $pmpro_currencies[ $pmpro_currency ]['position'] == 'left' ) {
			$formatted = $pmpro_currency_symbol . $formatted;
		} else {
			$formatted = $formatted . $pmpro_currency_symbol;
		}
	} else {
		$formatted = $pmpro_currency_symbol . $formatted;   // default to symbol on the left
	}

	// filter
	return apply_filters( 'pmpro_format_price', $formatted, $price, $pmpro_currency, $pmpro_currency_symbol );
}

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

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

/**
 * 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 non-account pages a subpage of account
			if ( $name != 'account' ) {
				$insert['post_parent'] = $pmpro_pages['account'];
			}

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

/**
 * Schedule a periodic event unless one with the same hook is already scheduled.
 *
 * @param int    $timestamp Timestamp for when to run the event.
 * @param string $recurrence How often the event should recur.
 * @param string $hook Action hook to execute when cron is run.
 * @param array  $args Optional. Arguments to pass to the hook's callback function.
 * @return false|void False when an event is not scheduled.
 * @since 1.8.7.3
 */
function pmpro_maybe_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) {
	$next = wp_next_scheduled( $hook, $args );
	if ( empty( $next ) ) {
		return wp_schedule_event( $timestamp, $recurrence, $hook, $args );
	} else {
		return false;
	}
}

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