<?php
#[AllowDynamicProperties]
class PMPro_Field {
	/**
	 * The name of the field.
	 *
	 * This is the name attribute of the input field and may be automatically prefixed if needed.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $name = '';

	/**
	 * The type of field that this is.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $type = '';

	/**
	 * The meta key for this field.
	 *
	 * Will be set to $name without any prefixes that are added.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $meta_key = '';

	/**
	 * The label of the field.
	 *
	 * This is the human-readable label displayed to the user.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $label = '';

	/**
	 * Whether the label should be shown.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $showmainlabel = true;

	/**
	 * A hint to be displayed with the field.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $hint = '';

	/**
	 * The membership levels that this field should be displayed for.
	 *
	 * @since 2.9
	 *
	 * @var array
	 */
	private $levels = array();

	/**
	 * Whether the field is required.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $required = false;

	/**
	 * Whether the field should be shown as required if $required is set to true.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $showrequired = true;

	/**
	 * Where this field should be shown.
	 *
	 * Options are true, false, 'admin', 'only', and 'only_admin'.
	 *
	 * @since 2.9
	 *
	 * @var mixed
	 */
	private $profile = true;

	/**
	 * Whether the field is readonly.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $readonly = false;

	/**
	 * Array to define conditions when a field should be shown or hidden.
	 *
	 * @since 2.9
	 *
	 * @var array
	 */
	private $depends = array();

	/**
	 * Flag to determine if depends conditions should be ANDed or ORed together.
	 *
	 * @since 2.9.1
	 *
	 * @var bool
	 */
	private $depends_or = false;

	/**
	 * Whether the field value should be sanitized before saving.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $sanitize = true;

	/**
	 * The ID to show for the field.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $id = '';

	/**
	 * Class for the input field.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $class = '';

	/**
	 * Class for the div wrapper for the input field.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $divclass = '';

	/**
	 * Whether this field should be included in a members list CSV export.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $memberslistcsv = false;

	/**
	 * The save function that should be used for this field.
	 *
	 * null defaults to the default save function.
	 *
	 * @since 2.9
	 *
	 * @var callable
	 */
	private $save_function = null;

	/**
	 * Whether this field should be shown when adding a member using
	 * the PMPro Add Member Add On.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $addmember = false;

	/**
	 * The size attribute when using a text input field type.
	 *
	 * @since 2.9
	 *
	 * @var int
	 */
	private $size = 30;

	/**
	 * The number of rows to show when using a textarea field type.
	 *
	 * @since 2.9
	 *
	 * @var int
	 */
	private $rows = 5;

	/**
	 * The number of columns to show when using a textarea field type.
	 *
	 * @since 2.9
	 *
	 * @var int
	 */
	private $cols = 80;

	/**
	 * The options for a select, select2, multiselect, checkbox_grouped, or radio field type.
	 *
	 * @since 2.9
	 *
	 * @var array
	 */
	private $options = array();

	/**
	 * Whether multiple options should be selectable when using a select, select2, or multiselect field type.
	 *
	 * @since 2.9
	 *
	 * @var bool
	 */
	private $multiple = false;

	/**
	 * The text to show next to a checkbox when using a checkbox field type.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $text = '';

	/**
	 * The HTML to show for an HTML field type.
	 *
	 * @since 2.9
	 *
	 * @var string
	 */
	private $html = '';

	/**
	 * The default value for a field.
	 *
	 * @since 3.2
	 *
	 * @var string
	 */
	private $default = '';

	/**
	 * File upload types.
	 * 
	 * @since 3.2
	 * 
	 * @var string
	 *
	 */
	private $allowed_file_types = '';

	/**
	 * File upload limit
	 * 
	 * @since 3.2
	 * 
	 * @var int
	 */
	private $max_file_size = '';

	function __construct($name = NULL, $type = NULL, $attr = NULL) {
		if ( ! empty( $name ) )
			return $this->set( $name, $type, $attr );
		else
			return true;
	}

	/**
	 * Magic getter to allow reading private class properties.
	 *
	 * @param string $name The property name.
	 * @return mixed The property value.
	 */
	function __get( $name ) {
		if ( isset( $this->$name ) ) {
			if ( ! $this->is_valid_property( $name ) ) {
				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The property %s is not valid for the field type %s.', 'paid-memberships-pro' ), esc_html( $name ), esc_html( $this->type ) ), '3.4' );
			}
			return $this->$name;
		} else {
			_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The property %s does not exist.', 'paid-memberships-pro' ), esc_html( $name ) ), '3.4' );
		}

		return null;
	}

	/**
	 * Magic setter to allow setting private class properties and
	 * throwing warnings when we want to phase out a property.
	 *
	 * @param string $name The property name.
	 * @param mixed $value The property value.
	 */
	function __set( $name, $value ) {
		if ( 'type' === $name ) {
			_doing_it_wrong( __FUNCTION__, esc_html__( 'PMPro_Field properties should not be modified directly and may break in a future version. Instead, create a new PMPro_Field object.', 'paid-memberships-pro' ), '3.4' );
		}

		$this->$name = $value;
	}
	
	/**
	 * Magic isset to check if a private class property is set.
	 *
	 * @param string $name The property name.
	 * @return bool Whether the property is set.
	 */
	function __isset( $name ) {
		return isset( $this->$name );
	}

	/**
	 * Magic __call to allow calling private class methods and throwing warnings
	 * when we want to phase out a method.
	 */
	function __call( $name, $arguments ) {
		switch( $name ) {
			case 'set':
				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the $args property of the constructor when creating a new PMPro_Field object.', 'paid-memberships-pro' ), esc_html( $name ) ), '3.4' );
				break;
			case 'saveUsersTable':
			case 'saveTermRelationshipsTable':
			case 'saveFile':
				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the save_field_for_user method of the PMPro_Field object.', 'paid-memberships-pro' ), esc_html( $name ) ), '3.4' );
				break;
			case 'getHTML':
			case 'getDependenciesJS':
				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the display() method of the PMPro_Field object.', 'paid-memberships-pro' ), esc_html( $name ) ), '3.4' );
				break;
			default:
				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version.', 'paid-memberships-pro' ), esc_html( $name ) ), '3.4' );
				break;
		}
		return call_user_func( array( $this, $name ), $arguments );
	}

	/**
	 * Check if a property should be present for the current field type.
	 *
	 * @since 3.4
	 *
	 * @param string $property The property to check.
	 * return bool Whether the property is valid for the field type.
	 */
	private function is_valid_property( $property ) {
		switch ( $property ) {
			case 'name':
			case 'type':
			case 'meta_key':
			case 'label':
			case 'showmainlabel':
			case 'hint':
			case 'levels':
			case 'required':
			case 'showrequired':
			case 'profile':
			case 'readonly':
			case 'depends':
			case 'depends_or':
			case 'sanitize':
			case 'id':
			case 'class':
			case 'divclass':
			case 'memberslistcsv':
			case 'save_function':
			case 'addmember':
			case 'default':
				return true;
				break;
			case 'size':
				return in_array( $this->type, array( 'text', 'number' ) );
				break;
			case 'rows':
			case 'cols':
				return 'textarea' === $this->type;
				break;
			case 'options':
				return in_array( $this->type, array( 'select', 'multiselect', 'select2', 'radio', 'checkbox_grouped' ) );
				break;
			case 'multiple':
				return in_array( $this->type, array( 'select', 'select2', 'multiselect' ) );
				break;
			case 'text':
				return 'checkbox' === $this->type;
				break;
			case 'html':
				return 'html' === $this->type;
				break;
			case 'allowed_file_types':
			case 'max_file_size':
				return 'file' === $this->type;
				break;
			default:
				return false;
				break;
		}
	}

	/*
		setup field based on passed values
		attr is array of one or more of the following:
		- size = int (size attribute for text fieldS)
		- required = bool (require this field at registration?)
		- options = array of strings (e.g. array("value"=>"option name", "value2" = "option 2 name"))
		- profile = mixed (show field in profile page? true for both, "admins" for admins only)
		- just_profile = bool (not required. true means only show field in profile)
		- class = string (class to add to html element)
	*/
	private function set($name, $type, $attr = array())
	{
		$this->name = $name;
		$this->type = $type;

		//set meta key
		$this->meta_key = $this->name;

		//add attributes as properties of this class
		if(!empty($attr))
		{
			foreach($attr as $key=>$value)
			{
				$this->$key = $value;
			}
		}

		//make sure levels is an array
		if ( ! empty( $this->levels ) && ! is_array( $this->levels ) ) {
			$this->levels = array( $this->levels );
		}

		//make sure we have an id
		if(empty($this->id))
			$this->id = $this->name;

		//fix class
		if(empty($this->class))
			$this->class = "pmpro_form_input";
		else
			$this->class .= " pmpro_form_input";

		//default label
		if(isset($this->label) && $this->label === false)
			$this->label = false;	//still false
		elseif(empty($this->label))
			$this->label = ucwords($this->name);

		if(!isset($this->showmainlabel))
			$this->showmainlabel = true;

		//add prefix to the name if equal to a query var
		$public_query_vars = array('m', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'static', 'pagename', 'page_id', 'error', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term', 'cpage', 'post_type', 'title', 'embed' );
		if(in_array($this->name, $public_query_vars))
			$this->name = "pmprorhprefix_" . $this->name;

		/**
		 * Legacy filter to define what field keys are saved to the wp_users table. 
		 *
		 * @deprecated 3.1
		 */
		$user_table_fields = apply_filters_deprecated( 'pmprorh_user_table_fields', array( array( 'user_url' ) ), '3.1', 'pmpro_user_table_fields' );

		/**
		 * Filter to define what field keys are saved to the wp_users table.
		 *
		 * @since 3.1
		 *
		 * @param array $user_table_fields Array of field keys saved to the wp_users table.
		 * @return array $user_table_fields Array of field keys saved to the wp_users table.
		 */
		$user_table_fields = apply_filters( 'pmpro_user_table_fields', $user_table_fields );

		// Save wp_users table fields to the WP_User, not usermeta.
		if ( in_array( $this->name, $user_table_fields ) ) {
			//use the save date function
			$this->save_function = array( $this, 'saveUsersTable' );
		}

		// Save user taxonomy fields to wp_term_relationships, not usermeta.
		if ( ! empty( $this->taxonomy ) ) {
			// Use the save taxonomy function.
			$this->save_function = array( $this, 'saveTermRelationshipsTable' );

			// Populate terms from the taxonomy if options are empty.
			if ( empty( $this->options ) ) {
				$terms = get_terms( $this->taxonomy, array( 'hide_empty' => false ) );
				if ( isset( $terms->errors ) ) {
					$this->options = array();
				} else {
					$terms_options = array();
					foreach ( $terms as $term ) {
						$terms_options[ $term->term_id ] = $term->name;
					}
					$this->options = $terms_options;
				}
			}
		}

		//default fields
		if($this->type == "text")
		{
			if(empty($this->size))
				$this->size = 30;
		}
		elseif($this->type == "number")
		{
			if(empty($this->size))
				$this->size = 5;
		}
		elseif($this->type == "select" || $type == "multiselect" || $type == "select2" || $type == "radio")
		{
			//default option
			if ( empty( $this->options ) )
				$this->options = array( '', esc_html__( '- choose one -', 'paid-memberships-pro' ) );

			/**
			 * Legacy filter to repair non-associative options.
			 *
			 * @deprecated 3.1
			 */
			$repair_non_associative_options = apply_filters_deprecated( 'pmprorh_repair_non_associative_options', array( true ), '3.1', 'pmpro_field_repair_non_associative_options' );

			/**
			 * Filter to repair non-associative options.
			 *
			 * @since 3.1
			 *
			 * @param bool $repair_non_associative_options Whether to repair non-associative options.
			 * @return bool $repair_non_associative_options Whether to repair non-associative options.
			 */
			$repair_non_associative_options = apply_filters( 'pmpro_field_repair_non_associative_options', $repair_non_associative_options );

			// Is a non associative array is passed? Set values to labels.
			if($repair_non_associative_options && !$this->is_assoc($this->options))
			{
				$newoptions = array();
				foreach($this->options as $option)
					$newoptions[$option] = $option;
				$this->options = $newoptions;
			}
		}
		elseif($this->type == "textarea")
		{
			if(empty($this->rows))
				$this->rows = 5;
			if(empty($this->cols))
				$this->cols = 80;
		}
		elseif($this->type == "file")
		{
			// Default to file preview if image type.
			if ( ! isset( $this->preview ) ) {
				$this->preview = true;
			}
			// Default to allow file delete if full source known.
			if ( ! isset( $this->allow_delete ) ) {
				$this->allow_delete = 'only_admin';
			}
			//use the file save function
			$this->save_function = array($this, "saveFile");
		}
		elseif($this->type == "checkbox")
		{
			if ( ! isset( $this->text ) || $this->text === '' )
			{
				$this->text = $this->label;
				$this->showmainlabel = false;
			}
		}
		elseif ( $this->type == 'hidden' ) {
			// Don't show the label for the hidden field.
			$this->showmainlabel = false;
		}

		return true;
	}

	/**
	 * Get the field value from $_REQEUST or $_SESSION.
	 * The value will be sanitized if the field has the sanitize property set to true.
	 *
	 * @since 3.4
	 *
	 * @return mixed The value of the field or null if not found.
	 */
	function get_value_from_request() {
		if ( isset( $_REQUEST[ $this->name ] ) ) {
			$value = $_REQUEST[$this->name]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		} elseif ( isset( $_REQUEST[ $this->name . '_checkbox' ] ) && $this->type == 'checkbox' ) {
			// Empty checkbox.
			$value = 0;
		} elseif ( ! empty( $_REQUEST[ $this->name . '_checkbox' ] ) && in_array( $this->type, array( 'checkbox_grouped', 'select2' ) ) )	{
			// Empty group checkboxes or select2.
			$value = array();
		} elseif ( isset( $_FILES[$this->name] ) && $this->type == 'file' ) {
			// File field.
			$value = $_FILES[$this->name]['name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}  elseif ( isset( $_SESSION[$this->name] ) ) {
			// Value stored in session.
			if ( is_array( $_SESSION[$this->name] ) && isset( $_SESSION[$this->name]['name'] ) ) {
				// File field in session.
				$_FILES[$this->name] = $_SESSION[$this->name]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				$value = $_SESSION[$this->name]['name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			} else {
				// Other field in session.
				$value = $_SESSION[$this->name]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			}

			// Clean up session.
			unset($_SESSION[$this->name]);
		} else {
			// No value found.
			return null;
		}

		// Sanitize the value if needed.
		if ( ! empty( $this->sanitize ) ) {
			if ( $this->type == 'textarea' ) {
				$value = sanitize_textarea_field( $value );
			} elseif ( is_array( $value ) ) {
				$value = array_map( 'sanitize_text_field', $value );
			} else {
				$value = sanitize_text_field( $value );
			}
		}

		return $value;
	}

	/**
	 * Whether this field stores multiple selected values as an array.
	 *
	 * @since 3.8.3
	 *
	 * @return bool
	 */
	public function stores_array_values() {
		if ( in_array( $this->type, array( 'checkbox_grouped', 'multiselect', 'select2' ), true ) ) {
			return true;
		}

		return ( 'select' === $this->type && ! empty( $this->multiple ) );
	}

	/**
	 * Normalize a multi-value field's stored value to an array of option keys.
	 *
	 * Profile/checkout saves store arrays. Members List CSV export joins those
	 * arrays with commas, and Import Users from CSV may write the cell back as a
	 * string. Display and comparison code need a real array either way.
	 *
	 * Does not unserialize. get_user_meta() already unserializes stored arrays,
	 * and this helper is also used on request-sourced values during checkout
	 * re-display.
	 *
	 * @since 3.8.3
	 *
	 * @param mixed $value Raw value (array from UI save, or CSV string).
	 * @return array List of selected option keys.
	 */
	public function get_values_as_array( $value ) {
		if ( is_array( $value ) ) {
			return $value;
		}

		if ( null === $value || false === $value || '' === $value ) {
			return array();
		}

		if ( ! is_string( $value ) ) {
			return array( $value );
		}

		// Comma-separated option keys from CSV import/export.
		// Option keys are expected not to contain commas (same as export).
		if ( false !== strpos( $value, ',' ) ) {
			$parts = array_map( 'trim', explode( ',', $value ) );
			return array_values( array_filter( $parts, 'strlen' ) );
		}

		return array( $value );
	}

	/**
	 * Save the field for a user.
	 *
	 * @since 3.4
	 *
	 * @param int $user_id The user ID to save the field for.
	 */
	function save_field_for_user( int $user_id ) {
		// Get the value of the field.
		$value = $this->get_value_from_request();

		// If field was not submitted, bail.
		if ( null === $value ) {
			return;
		}

		// Read-only fields are display-only; their value is managed server-side
		// (via update_user_meta by add-ons/admin tooling), never from the request.
		// Skipping the save here prevents users from tampering with fields they cannot edit.
		if ( 'readonly' === $this->type || ! empty( $this->readonly ) ) {
			return;
		}

		// Check if we have a save function.
		if ( ! empty( $this->save_function ) ) {
			// Call the save function.
			call_user_func( $this->save_function, $user_id, $this->name, $value, $this );
		} else {
			// Save the value to usermeta.
			update_user_meta($user_id, $this->meta_key, $value);
		}
	}

	// Save function for users table field.
	private function saveUsersTable( $user_id, $name, $value ) {
		// Special sanitization needed for certain user fields.
		if ( $name === 'user_url' ) {
			$value = esc_url_raw( $value );
		}
		if ( $name === 'user_nicename' ) {
			$value = sanitize_title( $value );
		}

		// Save updated profile fields.
		wp_update_user( array( 'ID' => $user_id, $name => $value ) );
	}

	// Save function for user taxonomy field.
	private function saveTermRelationshipsTable( $user_id, $name, $value ) {
		// Get the taxonomy to save for.
		if ( isset( $this->taxonomy ) ) {
			$taxonomy = $this->taxonomy;
		}

		// Convert all terms in the value submitted to slugs.
		$new_values = array();
		foreach ( (array)$value as $term ) {
			if ( is_numeric( $term ) ) {
				$term_object = get_term_by( 'ID', $term, $taxonomy );
				$new_values[] = $term_object->name;
			} else {
				$new_values[] = $term;
			}
		}

		// Sets the terms for the user.
		wp_set_object_terms( $user_id, $new_values, $taxonomy, false );

		// Remove the user taxonomy relationship to terms from the cache.
		clean_object_term_cache( $user_id, $taxonomy );

		// Save terms to usermeta.
		$meta_key = str_replace( 'pmprorhprefix_', '', $name );
		update_user_meta( $user_id, $meta_key, $value );
	}

	//save function for files
	private function saveFile($user_id, $name, $value)
	{
		//setup some vars
		$user = get_userdata($user_id);
		$meta_key = str_replace("pmprorhprefix_", "", $name);

		// deleting?
		if( isset( $_REQUEST['pmpro_delete_file_' . $name . '_field'] ) ) {
			$delete_old_file_name = sanitize_text_field( $_REQUEST['pmpro_delete_file_' . $name . '_field'] );
			if ( ! empty( $delete_old_file_name ) ) {
				// Use what's saved in user meta so we don't delete any old file.
				$old_file_meta = get_user_meta( $user->ID, $meta_key, true );
				if ( 
					! empty( $old_file_meta ) && 
					! empty( $old_file_meta['fullpath'] ) && 
					file_exists( $old_file_meta['fullpath'] ) &&
					$old_file_meta['filename'] ==  $delete_old_file_name
				) {
					unlink( $old_file_meta['fullpath'] );
					if ( ! empty( $old_file_meta['previewpath'] ) ) {
						unlink( $old_file_meta['previewpath'] );
					}
					delete_user_meta( $user->ID, $meta_key );
				}
			}
		}

		// If we don't have a file to upload, return.
		if ( empty( $_FILES[ $name ] ) || empty( $_FILES[ $name ]['name'] ) ) {
			return;
		}

		// Check if we can upload the file.
		$upload_check = pmpro_check_upload( $name );
		if ( is_wp_error( $upload_check ) ) {
			pmpro_setMessage( $upload_check->get_error_message(), 'pmpro_error' );
			return;
		}

		// Get $file and $filetype.
		$file = array_map( 'sanitize_text_field', $_FILES[ $name ] );
		$filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );;

		/*
			save file in uploads
		*/
		//check for a register helper directory in wp-content
		$upload_dir = wp_upload_dir();
		$dir_path = $upload_dir['basedir'] . "/pmpro-register-helper/" . $user->user_login . "/";
		$dir_url  = $upload_dir['baseurl'] . "/pmpro-register-helper/" . $user->user_login . "/";

		//create the dir and subdir if needed
		if(!is_dir($dir_path))
		{
			wp_mkdir_p($dir_path);
		}

		//if we already have a file for this field, delete it
		$old_file = get_user_meta($user->ID, $meta_key, true);
		if(!empty($old_file) && !empty($old_file['fullpath']) && file_exists($old_file['fullpath']))
		{
			unlink($old_file['fullpath']);
		}

		//figure out new filename
		$filename = sanitize_file_name( $file['name'] );
		$count = 0;

		while(file_exists($dir_path . $filename))
		{
			if($count)
				$filename = str_lreplace("-" . $count . "." . $filetype['ext'], "-" . strval($count+1) . "." . $filetype['ext'], $filename);
			else
				$filename = str_lreplace("." . $filetype['ext'], "-1." . $filetype['ext'], $filename);

			$count++;

			//let's not expect more than 50 files with the same name
			if($count > 50)
				die( esc_html__( "Error uploading file. Too many files with the same name.", "paid-memberships-pro" ) );
		}

		$file_path = $dir_path . $filename;
		$file_url = $dir_url . $filename;

		//save file
		if(strpos($file['tmp_name'], $upload_dir['basedir']) !== false)
		{
			//was uploaded and saved to $_SESSION
			rename($file['tmp_name'], $file_path);
		}
		else
		{
			// Make sure file was uploaded.
			if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
				pmpro_setMessage( sprintf( esc_html__( 'Sorry, the file %s was not uploaded.', 'paid-memberships-pro' ), $file['name'] ), 'pmpro_error' );
				return false;
			}

			//it was just uploaded
			move_uploaded_file($file['tmp_name'], $file_path);
		}

		// If file is an image, save a preview thumbnail.
		if ( $filetype && 0 === strpos( $filetype['type'], 'image/' ) ) {
			$preview_file = wp_get_image_editor( $file_path );
			if ( ! is_wp_error( $preview_file ) ) {
				$preview_file->resize( 400, 400, false );
				$preview_file->generate_filename( 'pmpro_file_preview' );
				$preview_file = $preview_file->save();
			}
		}

		// Swap slashes for Windows
		$file_path = str_replace( "\\", "/", $file_path );
		$file_url = str_replace( "\\", "/", $file_url );
		if ( ! empty( $preview_file ) && ! is_wp_error( $preview_file ) ) {
			$preview_file['path'] = str_replace( "\\", "/", $preview_file['path'] );
		}

		$file_meta_value_array = array(
			'original_filename'	=> $file['name'],
			'filename'			=> $filename,
			'fullpath'			=> $file_path,
			'fullurl'			=> $file_url,
			'size'				=> $file['size'],
		);

		if ( ! empty( $preview_file ) && ! is_wp_error( $preview_file ) ) {
			$file_meta_value_array['previewpath'] = $preview_file['path'];
			$file_meta_value_array['previewurl'] = $dir_url .  $preview_file['file'];
		}

		//save filename in usermeta
		update_user_meta($user_id, $meta_key, $file_meta_value_array );
	}

	/**
	 * Display the field.
	 */
	function display( $value = NULL ) {
		echo $this->getHTML( $value ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		$this->getDependenciesJS();
		return;
	}

	//get HTML for the field
	private function getHTML($value = "")
	{
		// Vars to store HTML to be added to the beginning or end.
		$r_beginning = '';
		$r_end = '';

		// Add the class to the div wrapper if in the admin.
		if ( is_admin() ) {
			$r_beginning .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field pmpro_form_field-' . esc_attr( $this->type ), 'pmpro_form_field-' . esc_attr( $this->type ) ) ) . '">';
			$r_end .= "</div>";
		}

		if ( '' === $value && pmpro_is_checkout() ) {
			/**
			 * Filter to set the default value for a field. The default value will only load if no value is already found.
			 * 
			 * @param string $value The default value for the field.
			 * @param object $this The field object.
			 * 
			 * @since 3.2
			 */
			$value = apply_filters( 'pmpro_field_default_value', $this->default, $this );
		}

		if($this->type == "text")
		{
			$r = '<input type="text" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . ( is_string( $value ) ? esc_attr(wp_unslash($value) ) : '' ) . '" ';
			if(!empty($this->size))
				$r .= 'size="' . esc_attr( $this->size ) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ' />';
		}
		elseif($this->type == "number")
		{
			$r = '<input type="number" pattern="\d+" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr($value) . '" ';
			if(!empty($this->size))
				$r .= 'size="' . esc_attr( $this->size ) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if ( empty( $this->html_attributes ) ) {
				$this->html_attributes = array();
			}
			// If custom values not available set the defaults
			if ( ! array_key_exists( 'min', $this->html_attributes ) ) {
				$this->html_attributes['min'] = '0';
			}
			if ( ! array_key_exists( 'step', $this->html_attributes ) ) {
				$this->html_attributes['step'] = '1';
			}
			$r .= $this->getHTMLAttributes();
			$r .= ' />';
		}
		elseif($this->type == "password")
		{
			$r = '<input type="password" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr(wp_unslash($value)) . '" ';
			if(!empty($this->size))
				$r .= 'size="' . esc_attr( $this->size ) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ' />';
		}
		elseif($this->type == "select")
		{
			// Multi-select values may be arrays (UI save) or CSV strings (import).
			if ( ! empty( $this->multiple ) ) {
				$value = $this->get_values_as_array( $value );
			}

			if(!empty($this->multiple))
				$r = '<select id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '[]" ';	//multiselect
			else
				$r = '<select id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" ';		//regular

			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'disabled="disabled" ';
			if(!empty($this->multiple))
				$r .= 'multiple="multiple" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ">\n";
			foreach($this->options as $ovalue => $option)
			{
				$r .= '<option value="' . esc_attr( trim( $ovalue ) ) . '" ';
				if(!empty($this->multiple) && in_array($ovalue, $value))
					$r .= 'selected="selected" ';
				elseif ( ! empty( $ovalue ) && is_string( $value ) && trim( $ovalue ) == trim( $value ) )
					$r .= 'selected="selected" ';
				$r .= '>' . esc_html( $option ) . "</option>\n";
			}
			$r .= '</select>';
		}
		elseif($this->type == "multiselect")
		{
			// Arrays from UI saves; CSV strings from import/export round-trips.
			$value = $this->get_values_as_array( $value );

			$r = '<select id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '[]" multiple="multiple" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'disabled="disabled" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ' size="3"'; 
			$r .= ">\n";
			foreach($this->options as $ovalue => $option)
			{
				$r .= '<option value="' . esc_attr($ovalue) . '" ';
				if(in_array( trim( $ovalue ), $value ) )
					$r .= 'selected="selected" ';
				$r .= '>' . esc_html( $option ) . "</option>\n";
			}
			$r .= '</select>';
		}
		elseif($this->type == "select2")
		{
			// Arrays from UI saves; CSV strings from import/export round-trips.
			$value = $this->get_values_as_array( $value );

			//build multi select
			$r = '<select id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '[]" multiple="multiple" style="width: 100%" ';
			if(isset($this->placeholder)) {
				$r .= 'data-placeholder="' . esc_attr($this->placeholder) . '" ';
				if(empty($this->select2options)) {
					$this->select2options = 'placeholder: "' . esc_attr($this->placeholder) . '"';
				}
			} else {
				$r .= 'data-placeholder="' . esc_html__('Choose one or more.', 'paid-memberships-pro') . '" ';
			}
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'disabled="disabled" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= '>';
			foreach($this->options as $ovalue => $option)
			{
				$r .= '<option value="' . esc_attr($ovalue) . '" ';
				if( in_array( trim( $ovalue ), $value ) )
					$r .= 'selected="selected" ';
				$r .= '>' . esc_html( $option ) . '</option>';
			}
			$r .= '</select>';
			$r .= '<input type="hidden" name="'. esc_attr( $this->name ) .'_checkbox" value="1" />';	// Extra field so we can track unchecked boxes. Naming just for consistency.

			if(!empty($this->select2options))
				$r .= '<script>jQuery(document).ready(function($){ $("#' . esc_attr( $this->id ) . '").select2({ ' . $this->select2options . ', theme: "classic", width: "resolve" }); });</script>';
			else
				$r .= '<script>jQuery(document).ready(function($){ $("#' . $this->id . '").select2( { theme: "classic", width: "resolve" }); });</script>';
		}
		elseif($this->type == "radio")
		{
			$count = 0;
			$r = '';
			$r .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-radio-items' ) ) . '">';
			foreach($this->options as $ovalue => $option)
			{
				$count++;
				$r .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field pmpro_form_field-radio-item' ) ) . '">';
				$r .= '<input type="radio" id="pmpro_field_' . esc_attr( $this->name . $count ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr($ovalue) . '" ';
				if(!empty($ovalue) && is_string($value) && trim( $ovalue ) == trim( $value ) )
					$r .= 'checked="checked"';
				if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
				if(!empty($this->readonly))
					$r .= 'disabled="disabled" ';
				if(!empty($this->html_attributes))
					$r .= $this->getHTMLAttributes();
				$r .= ' /> ';
				$r .= '<label class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_label pmpro_form_label-inline pmpro_clickable' ) ) . '" for="pmpro_field_' . esc_attr( $this->name . $count ) . '">' . esc_html( $option ) . '</label> &nbsp; ';
				$r .= '</div> <!-- end pmpro_form_field-radio-item -->';
			}
			$r .= '</div> <!-- end pmpro_form_field-radio-items -->';
		}
		elseif($this->type == "checkbox")
		{
			$r = '<label class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_label pmpro_form_label-inline pmpro_clickable' ) ) . '" for="' . esc_attr( $this->name ) . '">';
			$r .= '<input name="'. esc_attr( $this->name ) .'"' .' type="checkbox" value="1"'.' id="'. esc_attr( $this->id ) .'"';
			$r .= checked( $value, 1, false );
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'disabled="disabled" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ' /> ';
			$r .= wp_kses_post( $this->text );
			$r .= '</label>';
			$r .= '<input type="hidden" name="'. esc_attr( $this->name ) .'_checkbox" value="1" />';	//extra field so we can track unchecked boxes
		}

		elseif($this->type == "checkbox_grouped")
		{
			// Arrays from UI saves; CSV strings from import/export round-trips.
			$value = $this->get_values_as_array( $value );

			$r = '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-checkbox-grouped' ) ) . '">';
			$r .= '<ul class="' . esc_attr( pmpro_get_element_class( 'pmpro_list pmpro_list-plain' ) ) . '">';
			$counter = 1;
			foreach($this->options as $ovalue => $option)
			{
				$r .= '<li class="' . esc_attr( pmpro_get_element_class( 'pmpro_list_item' ) ) . '">';
				$r .= '<span class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-checkbox-grouped-item' ) ) . '">';
				$r .= sprintf(
					'<input name="%1$s[]" type="checkbox" value="%2$s" id="%3$s" class="%4$s" %5$s %6$s %7$s />',
					esc_attr( $this->name ),
					esc_html( $ovalue ),
					esc_attr( "{$this->id}_{$counter}" ),
					esc_attr( $this->class ),
					( in_array($ovalue, $value) ? 'checked="checked"' : null ),
					( !empty( $this->readonly ) ? 'disabled="disabled"' : null ),
					$this->getHTMLAttributes()
				);

				$r .= sprintf( ' <label class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_label pmpro_form_label-inline pmpro_clickable' ) ) . '" for="%1$s">%2$s</label>', esc_attr( "{$this->id}_{$counter}" ), esc_html( $option ) );
				$r .= sprintf( '<input type="hidden" name="%1$s_checkbox[]" value="%2$s" />', esc_attr( $this->name ), esc_attr( $ovalue ) );	//extra field so we can track unchecked boxes
				$counter++;
				$r .= sprintf( '</span></li>' );
			}

			$r .= sprintf( '</ul></div>' );

		}
		elseif($this->type == "textarea")
		{
			$r = '<textarea id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" rows="' . $this->rows . '" cols="' . esc_attr( $this->cols ) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
				$r .= '>' . ( ( is_string( $value ) ) ? esc_textarea(wp_unslash($value) ) : '' ) . '</textarea>';
		}
		elseif($this->type == "hidden")
		{
			$r = '<input type="hidden" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr(wp_unslash($value)) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= '/>';
		}
		elseif($this->type == "html")
		{
			//arbitrary html/etc
			if(!empty($this->html))
				$r = $this->html;
			else
				$r = "";
		}
		elseif($this->type == "file")
		{
			$r = '';

			// Show the existing file with a preview and allow user to delete or replace.
			if ( ! empty( $value ) && ( is_array( $value ) || ! empty( $this->file ) ) ) {
				if ( is_array( $value ) ) {
					$file = $value;
				} elseif ( ! empty( $this->file ) ) {
					// Legacy support for $this->file.
					$file = $this->file;
				}

				// Show a preview of existing file if image type.
				if ( ( ! empty( $this->preview ) ) && ! empty( $file['previewurl'] ) ) {
					$filetype = wp_check_filetype( basename( $file['previewurl'] ), null );
					if ( $filetype && 0 === strpos( $filetype['type'], 'image/' ) ) {
						$r_beginning .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-preview' ) ) . '"><img src="' . esc_url( $file['previewurl'] ) . '" alt="' . esc_attr( basename($file['filename']) ) . '" /></div>';
					}
				}

				if( ! empty( $file['fullurl'] ) ) {
					$r_beginning .= '<div class="pmpro_form_field-file-name pmpro_file_' . esc_attr( $this->name ) . '_name">' . sprintf(__('Current File: %s', 'paid-memberships-pro' ), '<a target="_blank" href="' . esc_url( $file['fullurl'] ) . '">' . esc_html( basename($file['filename']) ) . '</a>' ) . '</div>';
				} elseif( is_string( $value ) ) {
					$r_beginning .= sprintf(__('Current File: %s', 'paid-memberships-pro' ), basename($value) );
				}

				$r_beginning .= '<div class="pmpro_form_field-file-actions">';
				// Allow user to delete the uploaded file if we know the full location. 
				if ( ( ! empty( $this->allow_delete ) ) && ! empty( $file['fullurl'] ) ) {
					// Check whether the current user can delete the uploaded file based on the field attribute 'allow_delete'.
					if ( $this->allow_delete === true || 
						( $this->allow_delete === 'admins' || $this->allow_delete === 'only_admin' && current_user_can( 'manage_options' ) )
					) {
						$r_beginning .= '<button class="button is-destructive pmpro_btn pmpro_btn-delete" id="pmpro_delete_file_' . esc_attr( $this->name ) . '_button" onclick="return false;">' . esc_html__( 'Delete', 'paid-memberships-pro' ) . '</button>';
					}
				}

				if( empty( $this->readonly ) ) {
					$r_beginning .= '<button class="button button-secondary pmpro_btn pmpro_btn-secondary" id="pmpro_replace_file_' . esc_attr( $this->name ) . '_button" onclick="return false;">' . esc_html__( 'Replace', 'paid-memberships-pro' ) . '</button>';
					$r_beginning .= '<button class="button button-secondary pmpro_btn pmpro_btn-cancel" id="pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button" style="display: none;" onclick="return false;">' . esc_html__( 'Cancel', 'paid-memberships-pro' ) . '</button>';
					$r_beginning .= '<input id="pmpro_delete_file_' . esc_attr( $this->name ) . '_field" name="pmpro_delete_file_' . esc_attr( $this->name ) . '_field" type="hidden" value="0" />';
				}
				$r_beginning .= '</div>';
				//include script to change enctype of the form and allow deletion
				$r .= '
				<script>
					jQuery(document).ready(function() {
						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename( $file['filename'] ) ) . '");
							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
						});

						jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename( $file['filename'] ) ) . '");
							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").show();
						});

						jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val(0);
							// set file input field to empty.
							jQuery("#' . esc_attr( $this->id ) . '").val("");
							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "none");
							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").show();
							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").show();
							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").hide();
							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
						});

					});
				</script>
				';
			}

			$r .= '
				<script>
					jQuery(document).ready(function() {
						jQuery("#' . esc_attr( $this->id ) . '").closest("form").attr("enctype", "multipart/form-data");
					});
				</script>
				';
			$r .= '<div id="pmpro_file_' . esc_attr( $this->id ) . '_upload" class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-upload' ) ) . '" ' . ((empty($value) || is_string($value)) ? '' : 'style="display: none;"') . '>';
			$r .= '<input type="file" id="' . esc_attr( $this->id ) . '" ';
			
			if ( ! empty( $this->allowed_file_types ) ) {

				// Break it out into an array if it is possible.
				$allowed_file_array = explode( ',', $this->allowed_file_types );

				// loop through the allowed arrays and add a period if there isn't one, BUT skip this if it contains a /* pattern (we can assume that it's okay.)
				foreach( $allowed_file_array as $key => $allowed_file_type ) {
					if ( strpos( $allowed_file_type, '/' ) === false && strpos( $allowed_file_type, '.' ) !== 0 ) {
						$allowed_file_array[ $key ] = '.' . $allowed_file_type;
					}
				}

				// Convert it back to a comma-separated string before adding it to the HTML.
				$this->allowed_file_types = implode( ',', $allowed_file_array );

				$r .= 'accept="' . esc_attr( $this->allowed_file_types ) . '" ';
			}

			if ( ! empty( $this->class ) ) {
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			}

			if ( ! empty( $this->html_attributes ) ) {
				$r .= $this->getHTMLAttributes();
			}

			if ( ! empty( $this->readonly ) ) {
				$r .= 'disabled="disabled" ';
			}

			$r .= 'name="' . esc_attr( $this->name ) . '" />';
			$r .= '</div>';

		}
		elseif($this->type == "date")
		{
			$r = '<input type="date" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr(wp_unslash($value)) . '" ';
			if(!empty($this->class))
				$r .= 'class="' . esc_attr( $this->class ) . '" ';
			if(!empty($this->readonly))
				$r .= 'readonly="readonly" ';
			if(!empty($this->html_attributes))
				$r .= $this->getHTMLAttributes();
			$r .= ' />';
		}
		elseif($this->type == "readonly")
		{
			if ( empty( $value ) ) {
				$r = '&#8212;';
			} else {
				$r = esc_html( $value );
			}
		}
		else
		{
			$r = "Unknown type <strong>" . esc_attr( $this->type ) . "</strong> for field <strong>" . esc_attr( $this->name ) . "</strong>.";
		}

		//show required by default
		if(!empty($this->required) && !isset($this->showrequired))
			$this->showrequired = true;

		/**
		 * Legacy filter to show a field as required on the profile page.
		 *
		 * @deprecated 3.1 Use the pmpro_show_required_on_profile filter instead.
		 *
		 * @param bool $showrequired Whether to show the field as required on the profile page.
		 * @return bool Whether to show the field as required on the profile page.
		 */
		$show_required_on_profile = apply_filters_deprecated( 'pmprorh_show_required_on_profile', array( false, $this ), '3.1', 'pmpro_field_show_required_on_profile' );

		/**
		 * Filter to show a field as required on the profile page.
		 *
		 * @since 3.1
		 *
		 * @param bool $showrequired Whether to show the field as required on the profile page.
		 * @return bool Whether to show the field as required on the profile page.
		 */
		$show_required_on_profile = apply_filters( 'pmpro_field_show_required_on_profile', false, $this );

		// If the field is required and the showrequired attribute is set to false, don't show the required indicator.
		if ( defined('IS_PROFILE_PAGE') && IS_PROFILE_PAGE && ! $show_required_on_profile )
			$this->showrequired = false;

		if ( ! empty( $this->required ) && ! empty( $this->showrequired ) && is_string( $this->showrequired ) && $this->showrequired !== 'label' ) {
				$r .= $this->showrequired;
		}

		//anything meant to be added to the beginning or end?
		$r = $r_beginning . $r . $r_end;

		/**
		 * Legacy filter to allow hooking into the generated field HTML.
		 *
		 * @deprecated 3.1
		 *
		 * @param string      $r     The field HTML.
		 * @param PMPro_Field $field The field object.
		 */
		$r = apply_filters_deprecated( 'pmprorh_get_html', array( $r, $this ), '3.1', 'pmpro_field_get_html' );

		/**
		 * Filter to allow hooking into the generated field HTML.
		 *
		 * @since 3.1
		 *
		 * @param string      $r     The field HTML.
		 * @param PMPro_Field $field The field object.
		 * 
		 */
		$r = apply_filters( 'pmpro_field_get_html', $r, $this );

		return $r;
	}

	/**
	 * Get the HTML attributes for this field.
	 *
	 * Note: Attribute names and values are escaped for output.
	 *
	 * @return string The HTML attributes for this field.
	 */
	function getHTMLAttributes() {
		$html = '';

		if ( ! empty( $this->html_attributes ) ) {
			foreach ( $this->html_attributes as $name => $value ) {
				// Note: In the future, WP may introduce esc_attr_name(), but for now we'll do it ourselves.
				$attribute_name = wp_check_invalid_utf8( $name );
				$attribute_name = preg_replace( '/[^a-zA-Z0-9\-_\[\]]+/', '-', $attribute_name );

				$html .= sprintf(
					' %1$s="%2$s"',
					$attribute_name,
					esc_attr( $value )
				);
			}
		}

		return $html;
	}

	private function getDependenciesJS()
	{
		global $pmpro_user_fields;
		//dependencies
		if(!empty($this->depends))
		{
			//build the checks
			$checks_escaped = array();
			$binds = array();
			foreach($this->depends as $check)
			{
				if(!empty($check['id']))
				{
					// If checking checkbox_grouped, need to update the $check['id'] with index of option.
					$field_id = $check['id'];
					$depends_field = PMPro_Field_Group::get_field( $field_id );
					if ( empty( $depends_field ) ) {
						continue;
					}
				
					$depends_field_group = PMPro_Field_Group::get_group_for_field( $depends_field );
					if ( empty( $depends_field_group ) ) {
						continue;
					}

					// Let's simplify.
					switch ( $depends_field->type ) {
						case 'checkbox_grouped':
							// Find an input with the name of the field and the value of the option, then check if it is selected.
							// Don't use the ID.
							$checks_escaped[] = "jQuery('input[name=\"" . esc_js( $field_id ) . "[]\"][value=" . esc_js( $check['value'] ) . "]:checked').length > 0";
							$binds[] = "input[name=\"" . esc_js( $field_id ) . "[]\"]";
							break;
						case 'checkbox':
							$checks_escaped[] = "jQuery('#" . esc_html( $field_id ) . "').is(':checked') == " . ( empty( $check['value'] ) ? 'false' : 'true' );
							$binds[] = "#" . esc_html( $field_id );
							break;
						case 'radio':
							$checks_escaped[] = "jQuery('input[name=\"" . esc_js( $field_id ) . "\"][value=\"" . esc_js( $check['value'] ) . "\"]:checked').length > 0";
							$binds[] = "input[type=radio][name=\"" . esc_js( $field_id ) . "\"]";
							break;
						default:
							$checks_escaped[] = "jQuery('#" . esc_html( $field_id ) . "').val() == " . json_encode( $check['value'] ) . " || jQuery.inArray( jQuery('#" . esc_html( $field_id ) . "').val(), " . json_encode( $check['value'] ) . ") > -1";
							$binds[] = "#" . esc_html( $field_id );
							break;
					}
				}
			}

			if(!empty($checks_escaped) && !empty($binds)) {
			?>
			<script>
				//function to check and hide/show
				function pmpro_<?php echo esc_html( $this->id );?>_hideshow() {
					let checks = [];
					<?php
					foreach( $checks_escaped as $check_escaped ) {
					?>
					checks.push(<?php echo $check_escaped; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>);
					<?php
					}

					if ( $this->depends_or ) {
					?>
						let show = checks.indexOf(true) > -1;
					<?php
					} else {
					?>
						let show = checks.indexOf(false) === -1;
					<?php
					}
					?>
					if(show) {
						jQuery('#<?php echo esc_html( $this->id );?>_tr').show();
						jQuery('#<?php echo esc_html( $this->id );?>_div').show();
						jQuery('#<?php echo esc_html( $this->id );?>').removeAttr('disabled');
					} else {
						jQuery('#<?php echo esc_html( $this->id );?>_tr').hide();
						jQuery('#<?php echo esc_html( $this->id );?>_div').hide();
						jQuery('#<?php echo esc_html( $this->id );?>').attr('disabled', 'disabled');
					}
				}

				jQuery(document).ready(function() {
						//run on page load
						pmpro_<?php echo esc_html( $this->id );?>_hideshow();

						//and run when certain fields are changed
						jQuery('<?php echo implode(',', $binds); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>').bind('click change keyup', function() {
							pmpro_<?php echo esc_html( $this->id );?>_hideshow();
						});
				});
			</script>
			<?php
			}
		}
	}

	/**
	 * Display the field at checkout.
	 *
	 * @deprecated 3.4 Use PMPro_Field_Group::display() instead.
	 */
	function displayAtCheckout()
	{
		_deprecated_function( __METHOD__, '3.4', 'PMPro_Field_Group::display()' );
		global $current_user;

		if( null !== $this->get_value_from_request() ) {
			$value = $this->get_value_from_request();
		} elseif(!empty($current_user->ID) && metadata_exists("user", $current_user->ID, $this->meta_key)) {
			$value = get_user_meta($current_user->ID, $this->meta_key, true);
		} elseif ( ! empty( $current_user->ID ) ) {
			$userdata = get_userdata( $current_user->ID );
			if ( ! empty( $userdata->{$this->name} ) ) {
				$value = $userdata->{$this->name};
			} elseif(isset($this->value)) {
				$value = $this->value;
			} else {
				$value = '';
			}
		} elseif(isset($this->value)) {
			$value = $this->value;
		} else {
			$value = "";
		}

		// Fix divclass.
		if ( ! empty( $this->divclass ) ) {
			$this->divclass .= " ";
		}

		// Add a class to the field based on the type.
		$this->divclass .= "pmpro_form_field pmpro_form_field-" . $this->type;
		$this->class .= " pmpro_form_input-" . $this->type;

		// Add the required class to field.
		if ( ! empty( $this->required ) ) {
			$this->divclass .= " pmpro_form_field-required";
			$this->class .= " pmpro_form_input-required";
		}

		// Add the class to not show a field is required if set.
		if ( empty( $this->showrequired ) || is_string( $this->showrequired ) ) {
			$this->divclass .= " pmpro_form_field-hide-required";
		}

		// Run the class through the filter.
		$this->divclass = pmpro_get_element_class( $this->divclass );
		$this->class = pmpro_get_element_class( $this->class );

		// radio/checkbox_grouped render multiple inputs, so the group is named with role/aria instead of a <label for>.
		$pmpro_field_is_group = in_array( $this->type, array( 'radio', 'checkbox_grouped' ), true );
		?>
		<div id="<?php echo esc_attr( $this->id );?>_div" <?php if ( ! empty( $this->divclass ) ) { echo 'class="' . esc_attr( $this->divclass ) . '"'; } ?><?php if ( $pmpro_field_is_group && ! empty( $this->showmainlabel ) ) { echo ' role="group" aria-labelledby="' . esc_attr( $this->id ) . '_label"'; } ?>>
			<?php if(!empty($this->showmainlabel)) { ?>
				<?php if ( $pmpro_field_is_group ) { ?>
				<span id="<?php echo esc_attr( $this->id );?>_label" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_label' ) ); ?>">
				<?php } else { ?>
				<label class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_label' ) ); ?>" for="<?php echo esc_attr($this->name);?>">
				<?php } ?>
					<?php echo wp_kses_post( $this->label );?>
					<?php
						if(!empty($this->required) && !empty($this->showrequired) && $this->showrequired === 'label')
						{
						?><span class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_asterisk' ) ); ?>"> <abbr title="<?php esc_attr_e( 'Required Field' ,'paid-memberships-pro' ); ?>">*</abbr></span><?php
						}
					?>
				<?php echo $pmpro_field_is_group ? '</span>' : '</label>'; ?>
				<?php $this->display($value); ?>
			<?php } else { ?>
				<?php $this->display($value); ?>
			<?php } ?>

			<?php if(!empty($this->hint)) { ?>
				<p class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_hint' ) ); ?>"><?php echo wp_kses_post( $this->hint );?></p>
			<?php } ?>
		</div>
		<?php
	}

	/**
	 * @deprecated 3.4 Use PMPro_Field_Group::display() instead.
	 */
	function displayInProfile($user_id, $edit = NULL)
	{
		_deprecated_function( __METHOD__, '3.4', 'PMPro_Field_Group::display()' );
		global $current_user;
		if(metadata_exists("user", $user_id, $this->meta_key))
		{
			$value = get_user_meta($user_id, $this->meta_key, true);
		}
		elseif(!empty($this->value))
			$value = $this->value;
		else
			$value = "";
		?>
		<tr id="<?php echo esc_attr( $this->id );?>_tr">
			<th>
				<?php if ( ! empty( $this->showmainlabel ) ) { ?>
					<label for="<?php echo esc_attr($this->name);?>"><?php echo wp_kses_post( $this->label );?></label>
				<?php } ?>
			</th>
			<td>
				<?php
					if(current_user_can("edit_user", $user_id) && $edit !== false)
						$this->display($value);
					else
						echo "<div>" . wp_kses_post( $this->displayValue($value) ) . "</div>";
				?>
				<?php if(!empty($this->hint)) { ?>
					<p class="description"><?php echo wp_kses_post( $this->hint );?></p>
				<?php } ?>
			</td>
		</tr>
		<?php
	}

	/**
	 * Echo the value of the field based on type
	 * and taking into account fields with options.
	 * @param mixed $value The value to be shown.
	 * @param bool $echo Whether to echo the value or return it.
	 * @since 3.0 Shows files as links and added echo parameter.
	 */
	function displayValue( $value, $echo = true ) {
		// Build the output.
		$output = '';
		$allowed_html = array();

		// Switch on the type of field.
		switch( $this->type ) {
			case 'text':
			case 'textarea':
				// Make sure that the value is a string.
				$output = is_string( $value ) ? $value : '';

				// If the field is a URL, check if we should try to embed it or show it as a link.
				if ( wp_http_validate_url( $value ) ) {
					/**
					 * Filter whether links should be clickable, embedded, or shown as plain text.
					 *
					 * @since 3.4
					 *
					 * @param string $link_display_type The type of link display. Accepts 'embedded', 'clickable_link', 'clickable_label', or 'text'.
					 * @param string $value             The value to be shown.
					 * @param PMPro_Field $field	    Field object that the value is for.
					 */
					$link_display_type = apply_filters( 'pmpro_field_value_link_display_type', 'embedded', $value, $this );
					switch ( $link_display_type ) {
						case 'embedded':
							$url_embed = wp_oembed_get( $value );
							if ( ! empty( $url_embed ) ) {
								// Oembed returned a value.
								$output = $url_embed;
								$allowed_html = array(
									'iframe' => array(
										'src'             => true,
										'height'          => true,
										'width'           => true,
										'frameborder'     => true,
										'allowfullscreen' => true,
										'allow'           => true,
									),
									'script' => array(
										'type' => true,
										'src'  => true,
									),
								);
								break;
							}
							// If we got here, we can't embed. Fall through to clickable link.
						case 'clickable_link':
							$output = '<a href="' . esc_url( $value ) . '" target="_blank">' . esc_html( $value ) . '</a>';
							$allowed_html = array(
								'a' => array(
									'href'   => true,
									'target' => true,
								),
							);
							break;
						case 'clickable_label':
							$output = '<a href="' . esc_url( $value ) . '" target="_blank">' . esc_html( $this->label ) . '</a>';
							$allowed_html = array(
								'a' => array(
									'href'   => true,
									'target' => true,
								),
							);
							break;
						default:
							// Do nothing. The value is already set.
							$output = $value;
							break;
					}
				} else {
					$output = $value;
				}
				break;
			case 'checkbox':
				$output = $value ? esc_html__( 'Yes', 'paid-memberships-pro' ) : esc_html__( 'No', 'paid-memberships-pro' );
				break;
			case 'number':
					// Make sure that the value is a number.
					$output = is_numeric( $value ) ? number_format_i18n( $value ) : '';
					break;
			case 'date':
				$timestamp = strtotime( $value );
				$output = $timestamp ? date_i18n( get_option( 'date_format' ), $timestamp ) : '';
				break;
			case 'select':
			case 'multiselect':
			case 'select2':
			case 'radio':
			case 'checkbox_grouped':
				// Multi-value fields may be arrays (UI) or CSV strings (import).
				if ( $this->stores_array_values() ) {
					$value = $this->get_values_as_array( $value );
				} elseif ( ! is_array( $value ) ) {
					$value = array( $value );
				}
				if ( ! is_array( $this->options ) ) {
					$this->options = array();
				}
				$labels = array();
				foreach( $value as $item ) {
					$labels[] = array_key_exists( $item, $this->options ) ? $this->options[ $item ] : $item;
				}
				$output = implode( ', ', $labels );
				break;
			case 'file':
				// Validate the value.
				if ( empty( $value ) ) {
					$output = esc_html__( 'No file uploaded.', 'paid-memberships-pro' );
				} elseif ( ! is_array( $value ) || empty( $value['fullurl'] ) ) {
					$output = esc_html__( 'Invalid file data.', 'paid-memberships-pro' );
				} else {
					// We have a file. Determine how to display it.
					$file_type = wp_check_filetype($value['fullurl']);
					switch ( $file_type['type'] ) {
						case 'image/jpeg':
						case 'image/png':
						case 'image/gif':
							$output = '<div class="' . pmpro_get_element_class( 'pmpro_form_field-file-preview' ) . '"><img class="' . pmpro_get_element_class( 'pmpro_form_field-file-subtype_' . $file_type['ext'] ) . '" alt="" src="' . $value['previewurl'] . '"></div><div class="' . pmpro_get_element_class( 'pmpro_form_field-file-name' ) . '"><a href="' . $value['fullurl'] . '" target="_blank">' . $value['filename'] . '</a></div>';
							$allowed_html = array(
								'a' => array(
									'href' => array(),
									'title' => array(),
									'target' => array(),
								),
								'div' => array(
									'class' => array(),
								),
								'img' => array(
									'alt' => array(),
									'class' => array(),
									'src' => array(),
								),
								'span' => array(
									'class' => array(),
								),
							);
							break;
						case 'video/mpeg':
						case 'video/mp4':
							$output = do_shortcode('[video src="' . $value['fullurl'] . '"]');
							$allowed_html = array(
								'video' => array(
									'src'       => true,
									'poster'    => true,
									'width'     => true,
									'height'    => true,
									'preload'   => true,
									'controls'  => true,
									'autoplay'  => true,
									'loop'      => true,
									'muted'     => true,
								),
								'source' => array(
									'src'   => true,
									'type'  => true,
								),
							);
							break;
						case 'audio/mpeg':
						case 'audio/wav':
							$output = do_shortcode('[audio src="' . $value['fullurl'] . '"]');
							$allowed_html = array(
								'audio' => array(
									'src'       => true,
									'controls'  => true,
									'autoplay'  => true,
									'loop'      => true,
									'muted'     => true,
									'preload'   => true,
								),
								'source' => array(
									'src'   => true,
									'type'  => true,
								),
							);
							break;
						default:
							$output = '<a href="' . $value['fullurl'] . '" target="_blank"><img class="' . pmpro_get_element_class( 'pmpro_form_field-file-subtype_' . $file_type['ext'] ) . '" src="' . wp_mime_type_icon( $file_type['type'] ) . '"><div class="' . pmpro_get_element_class( 'pmpro_form_field-file-name' ) . '">' . $value['filename'] . '</div></a>';
							$allowed_html = array(
								'a' => array(
									'href' => array(),
									'target' => array(),
								),
								'img' => array(
									'class' => array(),
									'src' => array(),
								),
								'div' => array(
									'class' => array(),
								),
							);
							break;
					}
					// Wrap the output in a div.
					$output = '<div class="' . pmpro_get_element_class( 'pmpro_form_field-file-' . $file_type['ext'] ) . '">' . $output . '</div>';
				}
				break;
			default:
				$output = (string) $value;
				break;
		}

		// Enforce string as output.
		$output = (string) $output;

		if ( $echo ) {
			echo wp_kses( $output, $allowed_html );
		} else {
			return wp_kses( $output, $allowed_html );
		}
	}

	/**
	 * Defining associative as integer array keys incrementing from 0.
	 * 
	 * Based off of https://stackoverflow.com/a/173479.
	 *
	 * @param array $array The array to check if it is associative.
	 * @return bool True if the array is associative, false otherwise.
	 */
	private function is_assoc( $array ) {
		if ( empty( $array ) ) {
			return false;
		}
		return array_keys( $array ) !== range( 0, count( $array ) - 1) ;
	}

	/**
	 * @deprecated 3.4 Use PMPro_Field_Group::get_group_for_field() instead.
	 */
	static function get_checkout_box_name_for_field( $field_name ) {
		_deprecated_function( __METHOD__, '3.4', 'PMPro_Field_Group::get_group_for_field()' );
		$field = PMPro_Field_Group::get_field( $field_name );
		if ( empty( $field ) ) {
			return '';
		}
	
		$field_group = PMPro_Field_Group::get_group_for_field( $field );
		return $field_group ? $field_group->name : '';
	}

	/**
	 * @deprecated 3.4
	 */
	function was_present_on_checkout_page() {
		_deprecated_function( __METHOD__, '3.4' );
		// Check if checkout box that field is in is on page.
		$checkout_box = PMPro_Field_Group::get_group_for_field( $this );
		if ( empty( $checkout_box ) ) {
			// Checkout box does not exist.
			return false;
		}

		$user_fields_locations = array(
			'after_username',
			'after_password',
			'after_email',
			'after_captcha',
		);
		if ( is_user_logged_in() && in_array( $checkout_box->name, $user_fields_locations ) ) {
			// User is logged in and field is only for new users.
			return false;
		}

		// Check if field is hidden because of "depends" option.
		if ( ! empty( $this->depends ) ) {
			//build the checks
			$checks = array();
			foreach($this->depends as $check) {
				if( ! empty( $check['id'] ) && isset( $check['value'] ) ) {
					// We have a valid depends statement.
					if ( isset( $_REQUEST[ $check['id'] . '_checkbox' ] ) ) {
						// This fields depends on a checkbox or checkbox_grouped input.
						if ( isset( $_REQUEST[ $check['id'] ] ) && is_array( $_REQUEST[ $check['id'] ] ) ) {
							// checkbox_grouped input.
							if ( ! in_array( $check['value'], $_REQUEST[ $check['id'] ] ) ) {
								return false;
							}
						} elseif ( isset( $_REQUEST[ $check['id'] ] ) && '1' === $_REQUEST[ $check['id'] ] ) {
							// Single checkbox that is checked.
							if ( empty( $check['value'] ) ) {
								// Set to show field only if checkbox is unchecked.
								return false;
							}
						} else {
							// Single checkbox that is unchecked. Set to show field only if checkbox is checked.
							if ( ! empty( $check['value'] ) ) {
								return false;
							}
						}
					} elseif ( isset( $_REQUEST[ $check['id'] ] ) && $check['value'] != $_REQUEST[ $check['id'] ] ) {
						// This fields depends on another field type.
						return false;
					}
				}
			}
		}

		// Field should have been showed at checkout.
		return true;
	}

	/**
	 * Check if the field was filled if needed.
	 */
	function was_filled_if_needed() {
		// If the field is not required, skip it.
		if ( empty( $this->required ) ) {
			return true;
		}

		// If this field has a 'depends` attribute, check if the field was actually shown.
		if ( ! empty( $this->depends ) ) {
			foreach ( $this->depends as $check ) {
				// If the $check object isn't valid, skip it.
				if ( ! isset( $check->id ) || ! isset( $check->value ) ) {
					return true;
				}

				// Get the field to check.
				$check_field = PMPro_Field_Group::get_field( $check->id );
				if ( empty( $check_field ) ) {
					// The check field doesn't exist, so this field wasn't shown.
					return true;
				}

				// Get the value of the field.
				$check_field_value = $check_field->get_value_from_request();
				if ( null === $check_field_value ) {
					// The check field wasn't submitted, so this field wasn't shown.
					return true;
				}

				// If $check['value'] doesn't match the value of the field, skip this field.
				if ( is_array( $check_field_value ) ) {
					if ( ! in_array( $check->value, $check_field_value ) ) {
						return true;
					}
				} else {
					if ( $check->value !== $check_field_value ) {
						return true;
					}
				}
			}
		}

		// At this point, we know that the field needs to be filled.
		$value = $this->get_value_from_request();
		switch ( $this->type ) {
			case 'text':
			case 'textarea':
			case 'number':
				$filled = ( null !== $value && '' !== trim( $value ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				break;
			case 'file':
				if ( ! empty( $_FILES[ $this->name ]['name'] ) ) {
					$filled = true;
				} elseif ( ! empty( get_user_meta( get_current_user_id(), $this->name, true ) ) && empty( $_REQUEST['pmpro_delete_file_' . $this->name . '_field'] ) ) {
					$filled = true;
				} else {
					$filled = false;
				}
				break;
			default:
				$filled = ! empty( $value );
		}
		return $filled;
	}
}