<?php
/**
 * Core LifterLMS functions file
 *
 * @package LifterLMS/Functions
 *
 * @since 1.0.0
 * @version 5.0.0
 */

defined( 'ABSPATH' ) || exit;

require_once 'functions/llms-functions-access-plans.php';
require_once 'functions/llms-functions-deprecated.php';
require_once 'functions/llms-functions-forms.php';
require_once 'functions/llms-functions-locale.php';
require_once 'functions/llms-functions-options.php';
require_once 'functions/llms-functions-progression.php';
require_once 'functions/llms-functions-user-information-fields.php';

require_once 'functions/llms.functions.access.php';
require_once 'functions/llms.functions.certificate.php';
require_once 'functions/llms.functions.course.php';
require_once 'functions/llms.functions.currency.php';
require_once 'functions/llms.functions.log.php';
require_once 'functions/llms.functions.notice.php';
require_once 'functions/llms.functions.order.php';
require_once 'functions/llms.functions.page.php';
require_once 'functions/llms.functions.person.php';
require_once 'functions/llms.functions.privacy.php';
require_once 'functions/llms.functions.quiz.php';
require_once 'functions/llms.functions.template.php';
require_once 'functions/llms.functions.user.postmeta.php';

/**
 * Insert elements into an associative array after a specific array key
 *
 * If the requested key doesn't exit, the new item will be added to the end of the array.
 * If you need to insert at the beginning of an array use array_merge( $new_item, $orig_item ).
 *
 * @since 3.21.0
 *
 * @param array  $array       Original associative array.
 * @param string $after_key   Key name in original array to insert new item after.
 * @param string $insert_key  Key name of the item to be inserted.
 * @param mixed  $insert_item Value to be inserted.
 * @return array
 */
function llms_assoc_array_insert( $array, $after_key, $insert_key, $insert_item ) {

	$res = array();

	$new_item = array(
		$insert_key => $insert_item,
	);

	$index = array_search( $after_key, array_keys( $array ) );
	if ( false !== $index ) {
		$index++;

		$res = array_merge(
			array_slice( $array, 0, $index, true ),
			$new_item,
			array_slice( $array, $index, count( $array ) - 1, true )
		);
	} else {
		$res = array_merge( $array, $new_item );
	}

	return $res;

}

/**
 * Retrieve the current time based on specified type.
 *
 * This is a wrapper for the WP Core current_time which can be plugged
 * We plug this during unit testing to allow mocking the current time.
 *
 * The 'mysql' type will return the time in the format for MySQL DATETIME field.
 * The 'timestamp' type will return the current timestamp.
 * Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d').
 *
 * If $gmt is set to either '1' or 'true', then both types will use GMT time.
 * if $gmt is false, the output is adjusted with the GMT offset in the WordPress option.
 *
 * @since 3.4.0
 *
 * @param string   $type Type of time to retrieve. Accepts 'mysql', 'timestamp', or PHP date format string (e.g. 'Y-m-d').
 * @param int|bool $gmt  Optional. Whether to use GMT timezone. Default false.
 * @return int|string Integer if $type is 'timestamp', string otherwise.
 */
if ( ! function_exists( 'llms_current_time' ) ) {
	function llms_current_time( $type, $gmt = 0 ) {
		return current_time( $type, $gmt );
	}
}

/**
 * Do apply_filters( 'the_content', $content ) without actions adding their own content onto us...
 *
 * @param string $content Optional. The content. Default is empty string.
 * @return string
 * @since 3.16.10
 * @version 3.19.2
 */
if ( ! function_exists( 'llms_content' ) ) {
	function llms_content( $content = '' ) {
		$content = do_shortcode( shortcode_unautop( wpautop( convert_chars( wptexturize( $content ) ) ) ) );
		global $wp_embed;
		if ( $wp_embed && method_exists( $wp_embed, 'autoembed' ) ) {
			$content = $wp_embed->autoembed( $content );
		}
		return $content;
	}
}

/**
 * Mark a function as deprecated and inform when it is used.
 *
 * This function uses WP core's `_deprecated_function()`, logging to the LifterLMS log file
 * located at `wp-content/updloads/llms-logs/llms-{$hash}.log` instead of `wp-content/debug.log`.
 *
 * @since 2.6.0
 * @since 3.6.0 Unknown.
 * @since 4.4.0 Uses WP `_deprecated_function()` instead of duplicating its logic.
 *
 * @param string $function    Name of the deprecated function.
 * @param string $version     LifterLMS version that deprecated the function.
 * @param string $replacement Optional. Replacement function. Default is `null`.
 * @return void
 */
function llms_deprecated_function( $function, $version, $replacement = null ) {

	_deprecated_function( $function, $version, $replacement );

}

/**
 * Cron function to cleanup files in the LLMS_TMP_DIR
 *
 * Removes any files that are more than a day old.
 *
 * @since 3.18.0
 * @since 4.10.1 Use strict type comparisons.
 *
 * @return void
 */
function llms_cleanup_tmp() {

	$max_age = llms_current_time( 'timestamp' ) - apply_filters( 'llms_tmpfile_max_age', DAY_IN_SECONDS );

	$exclude = array( '.htaccess', 'index.html' );

	foreach ( glob( LLMS_TMP_DIR . '*' ) as $file ) {

		// Don't cleanup index and .htaccess.
		if ( in_array( basename( $file ), $exclude, true ) ) {
			continue;
		}

		if ( filemtime( $file ) < $max_age ) {
			wp_delete_file( $file );
		}
	}

}
add_action( 'llms_cleanup_tmp', 'llms_cleanup_tmp' );

if ( ! function_exists( 'llms_filter_input' ) ) {

	/**
	 * Gets a specific external variable by name and optionally filters it
	 *
	 * This is a pluggable wrapper around native `filter_input` which is plugged in the testing framework
	 * to allow easy mocking of form variables when testing form controller functions and methods.
	 *
	 * @since 3.29.0
	 *
	 * @param int    $type          One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
	 * @param string $variable_name Name of a variable to get.
	 * @param int    $filter        Optional. The ID of the filter to apply. Default is `FILTER_DEFAULT`.
	 * @param mixed  $options       Optional. Associative array of options or bitwise disjunction of flags. Default is empty array.
	 *                              If filter accepts options, flags can be provided in "flags" field of array.
	 * @return mixed  Value of the requested variable on success, FALSE if the filter fails, or NULL if
	 *                the variable_name variable is not set. If the flag FILTER_NULL_ON_FAILURE is used,
	 *                it returns FALSE if the variable is not set and NULL if the filter fails.
	 */
	function llms_filter_input( $type, $variable_name, $filter = FILTER_DEFAULT, $options = array() ) {
		return filter_input( $type, $variable_name, $filter, $options );
	}
}

/**
 * Retrieve an array of post types which can be completed by students
 *
 * @since 4.2.0
 *
 * @return string[]
 */
function llms_get_completable_post_types() {

	/**
	 * Filter the list of post types which can be completed by students.
	 *
	 * @since Unknown
	 *
	 * @param string[] $post_types WP_Post post type names.
	 */
	return apply_filters( 'llms_completable_post_types', array( 'course', 'section', 'lesson' ) );

}

/**
 * Retrieve an array of taxonomies which can be completed by students
 *
 * @since 4.2.0
 *
 * @return string[]
 */
function llms_get_completable_taxonomies() {

	/**
	 * Filter the list of taxonomies which can be completed by students.
	 *
	 * @since 4.2.0
	 *
	 * @param string[] $taxonomies Taxonomy names.
	 */
	return apply_filters( 'llms_completable_taxonomies', array( 'course_track' ) );

}

/**
 * Retrieve an array of post types whose name doesn't start with the prefix 'llms_'.
 *
 * @since 4.10.1
 *
 * @return string[]
 */
function llms_get_unprefixed_post_types() {

	/**
	 * Filter the list of post types whose name doesn't start with the prefix 'llms_'.
	 *
	 * @since 4.10.1
	 *
	 * @param string[] $post_types WP_Post post type names.
	 */
	return apply_filters( 'llms_unprefixed_post_types', array( 'course', 'section', 'lesson' ) );

}

/**
 * Get themes natively supported by LifterLMS
 *
 * @since 3.0.0
 *
 * @return array
 */
function llms_get_core_supported_themes() {
	return array(
		'canvas',
		'Divi',
		'genesis',
		'twentyseventeen',
		'twentysixteen',
		'twentyfifteen',
		'twentyfourteen',
		'twentythirteen',
		'twentyeleven',
		'twentytwelve',
		'twentyten',
	);
}

/**
 * Get human readable time difference between 2 dates
 *
 * Return difference between 2 dates in year, month, hour, minute or second
 * The $precision caps the number of time units used: for instance if
 * $time1 - $time2 = 3 days, 4 hours, 12 minutes, 5 seconds
 * - with precision = 1 : 3 days
 * - with precision = 2 : 3 days, 4 hours
 * - with precision = 3 : 3 days, 4 hours, 12 minutes.
 *
 * @since Unknown
 * @since 3.24.0 Unknown.
 *
 * @source http://www.if-not-true-then-false.com/2010/php-calculate-real-differences-between-two-dates-or-timestamps/
 *
 * @param mixed   $time1     A time (string or timestamp).
 * @param mixed   $time2     A time (string or timestamp).
 * @param integer $precision Optional precision. Default is 2.
 * @return string time difference
 */
function llms_get_date_diff( $time1, $time2, $precision = 2 ) {
	// If not numeric then convert timestamps.
	if ( ! is_numeric( $time1 ) ) {
		$time1 = strtotime( $time1 );
	}
	if ( ! is_numeric( $time2 ) ) {
		$time2 = strtotime( $time2 );
	}
	// If time1 > time2 then swap the 2 values.
	if ( $time1 > $time2 ) {
		list( $time1, $time2 ) = array( $time2, $time1 );
	}
	// Set up intervals and diffs arrays.
	$intervals     = array( 'year', 'month', 'day', 'hour', 'minute', 'second' );
	$l18n_singular = array(
		'year'   => __( 'year', 'lifterlms' ),
		'month'  => __( 'month', 'lifterlms' ),
		'day'    => __( 'day', 'lifterlms' ),
		'hour'   => __( 'hour', 'lifterlms' ),
		'minute' => __( 'minute', 'lifterlms' ),
		'second' => __( 'second', 'lifterlms' ),
	);
	$l18n_plural   = array(
		'year'   => __( 'years', 'lifterlms' ),
		'month'  => __( 'months', 'lifterlms' ),
		'day'    => __( 'days', 'lifterlms' ),
		'hour'   => __( 'hours', 'lifterlms' ),
		'minute' => __( 'minutes', 'lifterlms' ),
		'second' => __( 'seconds', 'lifterlms' ),
	);
	$diffs         = array();
	foreach ( $intervals as $interval ) {
		// Create temp time from time1 and interval.
		$ttime = strtotime( '+1 ' . $interval, $time1 );
		// Set initial values.
		$add    = 1;
		$looped = 0;
		// Loop until temp time is smaller than time2.
		while ( $time2 >= $ttime ) {
			// Create new temp time from time1 and interval.
			$add++;
			$ttime = strtotime( '+' . $add . ' ' . $interval, $time1 );
			$looped++;
		}
		$time1              = strtotime( '+' . $looped . ' ' . $interval, $time1 );
		$diffs[ $interval ] = $looped;
	}
	$count = 0;
	$times = array();
	foreach ( $diffs as $interval => $value ) {
		// Break if we have needed precision.
		if ( $count >= $precision ) {
			break;
		}
		// Add value and interval if value is bigger than 0.
		if ( $value > 0 ) {
			if ( 1 != $value ) {
				$text = $l18n_plural[ $interval ];
			} else {
				$text = $l18n_singular[ $interval ];
			}
			// Add value and interval to times array.
			$times[] = $value . ' ' . $text;
			$count++;
		}
	}
	// Return string with times.
	return implode( ', ', $times );
}

/**
 * Instantiate an instance of DOMDocument with an HTML string
 *
 * This function suppresses PHP warnings that would be thrown by DOMDocument when
 * loading a partial string or an HTML string with errors.
 *
 * @see LLMS_DOM_Document->load().
 *
 * @since 4.7.0
 * @since 4.8.0 Remove reliance on `mb_convert_encoding()`.
 * @since 4.13.0 Add back partial reliance on `mb_convert_encoding()` but keep the previous implementation as a fall-back.
 *               Also fix a potential fatal in the fall-back which tried to manipulate a non existent node.
 *               Wrapper for `LLMS_Dom_Document:load()`.
 *
 * @param string $string An HTML string, either a full HTML document or a partial string.
 * @return DOMDocument|WP_Error Returns an instance of DOMDocument with `$string` loaded into it
 *                              or an error object when DOMDocument isn't available or an error is encountered during loading.
 */
function llms_get_dom_document( $string ) {

	$llms_dom = new LLMS_DOM_Document( $string );
	$load     = $llms_dom->load();

	return is_wp_error( $load ) ? $load : $llms_dom->dom();
}

/**
 * Retrieve the HTML for a donut chart
 *
 * Note that this must be used in conjunction with some JS to initialize the chart!
 *
 * @since 3.9.0
 * @since 3.24.0 Unknown.
 *
 * @param mixed  $percentage Percentage to display
 * @param string $text       Optional. Text/caption to display (short). Default is empty string.
 * @param string $size       Optional. Size of the chart (mini, small, default, large). Default is 'default'.
 * @param array  $classes    Optional. Additional custom css classes to add to the chart element. Default is empty array.
 * @return string
 */
function llms_get_donut( $percentage, $text = '', $size = 'default', $classes = array() ) {
	$percentage = is_numeric( $percentage ) ? $percentage : 0;
	$classes    = array_merge( array( 'llms-donut', $size ), $classes );
	$classes    = implode( ' ', $classes );
	$percentage = 'mini' === $size ? round( $percentage, 0 ) : LLMS()->grades()->round( $percentage );
	return '
		<div class="' . $classes . '" data-perc="' . $percentage . '">
			<div class="inside">
				<div class="percentage">
					' . $percentage . '<small>%</small>
					<div class="caption">' . $text . '</div>
				</div>
			</div>
		</div>';
}

/**
 * Get a list of registered engagement triggers
 *
 * @return array
 * @since 3.1.0
 * @since 3.24.1
 */
function llms_get_engagement_triggers() {
	/**
	 * Filter the engagement triggers
	 *
	 * @since Unknown
	 *
	 * @param array $engagement_triggers An associative array of engagement triggers. Keys are the engagement trigger slugs, values are their description.
	 */
	return apply_filters(
		'lifterlms_engagement_triggers',
		array(
			'user_registration'      => __( 'Student creates a new account', 'lifterlms' ),
			'access_plan_purchased'  => __( 'Student Purchases an Access Plan', 'lifterlms' ),
			'course_enrollment'      => __( 'Student enrolls in a course', 'lifterlms' ),
			'course_purchased'       => __( 'Student purchases a course', 'lifterlms' ),
			'course_completed'       => __( 'Student completes a course', 'lifterlms' ),
			// 'days_since_login' => __( 'Days since user last logged in', 'lifterlms' ), // @todo.
			'lesson_completed'       => __( 'Student completes a lesson', 'lifterlms' ),
			'quiz_completed'         => __( 'Student completes a quiz', 'lifterlms' ),
			'quiz_passed'            => __( 'Student passes a quiz', 'lifterlms' ),
			'quiz_failed'            => __( 'Student fails a quiz', 'lifterlms' ),
			'section_completed'      => __( 'Student completes a section', 'lifterlms' ),
			'course_track_completed' => __( 'Student completes a course track', 'lifterlms' ),
			'membership_enrollment'  => __( 'Student enrolls in a membership', 'lifterlms' ),
			'membership_purchased'   => __( 'Student purchases a membership', 'lifterlms' ),
		)
	);
}

/**
 * Get a list of registered engagement types
 *
 * @return array
 * @since 3.1.0
 * @version 3.24.0
 */
function llms_get_engagement_types() {
	/**
	 * Filter the engagement types
	 *
	 * @since Unknown
	 *
	 * @param array $engagement_types An associative array of engagement types. Keys are the engagement type slugs, values are their description.
	 */
	return apply_filters(
		'lifterlms_engagement_types',
		array(
			'achievement' => __( 'Award an Achievement', 'lifterlms' ),
			'certificate' => __( 'Award a Certificate', 'lifterlms' ),
			'email'       => __( 'Send an Email', 'lifterlms' ),
		)
	);
}

/**
 * Retrieve a list of post types which users can be enrolled into.
 *
 * @since 4.4.1
 *
 * @return string[] A list of post type names.
 */
function llms_get_enrollable_post_types() {

	/**
	 * Customize the post types which users can be enrolled into.
	 *
	 * This filter differs slightly from `llms_user_enrollment_status_allowed_post_types`. This filter
	 * determines which post types a user can be physically associated with through enrollment while
	 * `llms_user_enrollment_status_allowed_post_types` allows checking of user enrollment based on
	 * posts which are associated with a post type.
	 *
	 * @since 3.37.9
	 *
	 * @see llms_user_enrollment_status_allowed_post_types
	 *
	 * @param string[] $post_types Array of post type names.
	 */
	return apply_filters( 'llms_user_enrollment_allowed_post_types', array( 'course', 'llms_membership' ) );

}

/**
 * Retrieve a list of post types that can be used to check a users enrollment status in an enroll-able post type.
 *
 * @since 4.4.1
 *
 * @return string[] A list of post type names.
 */
function llms_get_enrollable_status_check_post_types() {

	/**
	 * Customize the post types that can be used to check a user's enrollment status.
	 *
	 * This filter differs slightly from `llms_user_enrollment_allowed_post_types`. The difference is that
	 * a user can be enrolled into a course but we can check their course enrollment status using the ID of a child (section or lesson).
	 *
	 * When adding a new post type for custom enrollment functionality the post type should be registered with
	 * both of these filters.
	 *
	 * @since 3.37.9
	 *
	 * @see llms_user_enrollment_allowed_post_types
	 *
	 * @param string[] $post_types List of allowed post types names.
	 */
	return apply_filters( 'llms_user_enrollment_status_allowed_post_types', array( 'course', 'section', 'lesson', 'llms_membership' ) );

}

/**
 * Retrieve an HTML anchor for an option page
 *
 * @since 3.18.0
 *
 * @param string $option_name Option name.
 * @param string $target      Optional. HTML target attribute. Defaults to _blank.
 * @return string
 */
function llms_get_option_page_anchor( $option_name, $target = '_blank' ) {

	$page_id = get_option( $option_name );

	if ( ! $page_id ) {
		return '';
	}

	$target = $target ? ' target="' . esc_attr( $target ) . '"' : '';

	return sprintf(
		'<a href="%1$s"%2$s>%3$s</a>',
		get_the_permalink( $page_id ),
		$target,
		get_the_title( $page_id )
	);

}

/**
 * Get a list of available product (course & membership) catalog visibility options
 *
 * @since 3.6.0
 *
 * @return array
 */
function llms_get_product_visibility_options() {
	/**
	 * Filter the product visibility options
	 *
	 * @since 3.6.0
	 *
	 * @param array $product_visibility_options. An associative array representing of visibility options. Keys are the engagement type slugs, values are their description.
	 */
	return apply_filters(
		'lifterlms_product_visibility_options',
		array(
			'catalog_search' => __( 'Catalog &amp; Search', 'lifterlms' ),
			'catalog'        => __( 'Catalog only', 'lifterlms' ),
			'search'         => __( 'Search only', 'lifterlms' ),
			'hidden'         => __( 'Hidden', 'lifterlms' ),
		)
	);
}

/**
 * Get an array of student IDs based on enrollment status a course or membership
 *
 * @since 3.0.0
 * @since 3.8.0 Unknown.
 * @since 4.10.2 Instantiate the student query passing `no_found_rows` arg as `true`,
 *               as we don't need (and do not return) pagination info, e.g. max_pages.
 *
 * @param int          $post_id  WP_Post id of a course or membership.
 * @param string|array $statuses List of enrollment statuses to query by status query is an OR relationship. Default is 'enrolled'.
 * @param integer      $limit    Number of results.
 * @param integer      $skip     Number of results to skip (for pagination).
 * @return array
 */
function llms_get_enrolled_students( $post_id, $statuses = 'enrolled', $limit = 50, $skip = 0 ) {

	$query = new LLMS_Student_Query(
		array(
			'post_id'       => $post_id,
			'statuses'      => $statuses,
			'page'          => ( 0 === $skip ) ? 1 : ( $skip / $limit ) + 1,
			'per_page'      => $limit,
			'sort'          => array(
				'id' => 'ASC',
			),
			'no_found_rows' => true,
		)
	);

	if ( $query->results ) {
		return wp_list_pluck( $query->results, 'id' );
	}

	return array();
}

/**
 * Retrieve default instructor data structure.
 *
 * @since 3.25.0
 *
 * @return array
 */
function llms_get_instructors_defaults() {
	/**
	 * Filter the instructor's default data structure.
	 *
	 * @since 3.25.0
	 *
	 * @param array $product_visibility_options. An associative array representing the instructor's default data structure.
	 */
	return apply_filters(
		'llms_post_instructors_get_defaults',
		array(
			'label'      => __( 'Author', 'lifterlms' ),
			'visibility' => 'visible',
			'id'         => '',
		)
	);
}

/**
 * Get the most recently created coupon ID for a given code
 *
 * @param string $code        Optional. The coupon's code (title). Default is empty string.
 * @param int    $dupcheck_id Optional. Coupon id that can be passed which will be excluded during the query
 *                            this is used to dupcheck the coupon code during coupon creation. Default is 0.
 * @return int
 * @since   3.0.0
 * @version 3.0.0
 */
function llms_find_coupon( $code = '', $dupcheck_id = 0 ) {

	global $wpdb;
	return $wpdb->get_var(
		$wpdb->prepare(
			"SELECT ID
		 FROM {$wpdb->posts}
		 WHERE post_title = %s
		 AND post_type = 'llms_coupon'
		 AND post_status = 'publish'
		 AND ID != %d
		 ORDER BY ID desc;
		",
			array( $code, $dupcheck_id )
		)
	); // no-cache ok.

}

/**
 * Get a list of available course / membership enrollment statuses
 *
 * @since 3.0.0
 *
 * @return array
 */
function llms_get_enrollment_statuses() {
	/**
	 * Filter the enrollment statuses
	 *
	 * @since 3.0.0
	 *
	 * @param array $enrollment_statuses An associative array representing the enrollment statuses. Keys are the statuses, values are their human readable labels (names).
	 */
	return apply_filters(
		'llms_get_enrollment_statuses',
		array(
			'cancelled' => __( 'Cancelled', 'lifterlms' ),
			'enrolled'  => __( 'Enrolled', 'lifterlms' ),
			'expired'   => __( 'Expired', 'lifterlms' ),
		)
	);

}

/**
 * Get the human readable (and translated) name of an enrollment status
 *
 * @since 3.0.0
 * @since 3.6.0 Unknown.
 *
 * @param string $status Enrollment status key.
 * @return string
 */
function llms_get_enrollment_status_name( $status ) {

	$status   = strtolower( $status ); // Backwards compatibility.
	$statuses = llms_get_enrollment_statuses();
	if ( is_array( $statuses ) && isset( $statuses[ $status ] ) ) {
		$status = $statuses[ $status ];
	}
	/**
	 * Filter the enrollment status name
	 *
	 * @since Unknown
	 *
	 * @param array $enrollment_status The enrollment status name.
	 */
	return apply_filters( 'lifterlms_get_enrollment_status_name', $status );

}

/**
 * Retrieve an IP Address for the current user
 *
 * @since 3.0.0
 * @since 3.35.0 Sanitize superglobal input.
 *
 * @return string
 */
function llms_get_ip_address() {

	$ip = '';

	// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Look below you.
	// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Look below you.
	if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {
		$ip = $_SERVER['HTTP_X_REAL_IP'];
	} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
		// Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2.
		// Make sure we always only send through the first IP in the list which should always be the client IP.
		$ip = trim( current( explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
	} elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
		$ip = $_SERVER['REMOTE_ADDR'];
	}
	// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash

	$ip = sanitize_text_field( wp_unslash( $ip ) );

	if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
		return '';
	}

	return $ip;

}

/**
 * Retrieves and filters the value open registration option
 *
 * @since 5.0.0
 *
 * @return string The value of the open registration status. Either "yes" for enabled or "no" for disabled.
 */
function llms_get_open_registration_status() {

	$status = get_option( 'lifterlms_enable_myaccount_registration', 'no' );

	/**
	 * Filter the value of the open registration setting
	 *
	 * @since 3.37.10
	 *
	 * @param string $status The current value of the open registration option. Either "yes" for enabled or "no" for disabled.
	 */
	return apply_filters( 'llms_enable_open_registration', $status );

}

/**
 * Retrieve the LLMS Post Model for a give post by ID or WP_Post Object
 *
 * @since 3.3.0
 * @since 3.16.11 Unknown.
 * @since 4.10.1 Made sure to only instantiate LifterLMS classes.
 *
 * @param WP_Post|int $post  Instance of WP_Post or a WP Post ID.
 * @param mixed       $error Determine what to return if the LLMS class isn't found.
 *                           post  = WP_Post
 *                           falsy = false.
 * @return LLMS_Post_Model|WP_Post|null|false LLMS_Post_Model extended object,
 *                                            null if WP get_post() fails,
 *                                            WP_Post if LLMS_Post_Model extended class isn't found and $error = 'post'
 *                                            false if LLMS_Post_Model extended class isn't found and $error != 'post'.
 */
function llms_get_post( $post, $error = false ) {

	$post = get_post( $post );
	if ( ! $post ) {
		return $post;
	}

	$class = '';

	// Check whether it's an llms post candidate: `post_type` starts with the 'llms_' prefix, or is one of the unprefixed ones.
	if ( 0 === strpos( $post->post_type, 'llms_' ) || in_array( $post->post_type, llms_get_unprefixed_post_types(), true ) ) {
		$post_type = explode( '_', str_replace( 'llms_', '', $post->post_type ) );
		$class     = 'LLMS';
		foreach ( $post_type as $part ) {
			$class .= '_' . ucfirst( $part );
		}
	}

	if ( $class && class_exists( $class ) ) {
		return new $class( $post );
	} elseif ( 'post' === $error ) {
		return $post;
	}

	return false;

}

/**
 * Retrieve the parent course for a section, lesson, or quiz
 *
 * @since 3.6.0
 * @since 3.17.7 Unknown.
 * @since 3.37.14 Bail if `$post` is not an istance of `LLMS_Post_Model`.
 *                Use strict comparison.
 *
 * @param WP_Post|int $post WP Post ID or instance of WP_Post.
 * @return LLMS_Course|null Instance of the LLMS_Course or null.
 */
function llms_get_post_parent_course( $post ) {

	$post = llms_get_post( $post );

	if ( ! $post || ! is_a( $post, 'LLMS_Post_Model' ) ) {
		return null;
	}

	/**
	 * Filter the course children post types
	 *
	 * @since Unknown
	 *
	 * @param $post_type string[] Names of the post types that can be children of a course.
	 */
	$post_types = apply_filters( 'llms_course_children_post_types', array( 'section', 'lesson', 'llms_quiz' ) );
	if ( ! in_array( $post->get( 'type' ), $post_types, true ) ) {
		return null;
	}

	/** @var LLMS_Section|LLMS_Lesson|LLMS_Quiz $post */
	return $post->get_course();

}


/**
 * Retrieve an array of existing transaction statuses
 *
 * @since 3.0.0
 *
 * @return array
 */
function llms_get_transaction_statuses() {
	/**
	 * Filter the transaction statuses
	 *
	 * @since Unknown
	 *
	 * @param $statuses string[] Names of the possible transaction statuses.
	 */
	return apply_filters(
		'llms_get_transaction_statuses',
		array(
			'llms-txn-failed',
			'llms-txn-pending',
			'llms-txn-refunded',
			'llms-txn-succeeded',
		)
	);
}

/**
 * Determine is request is an ajax request
 *
 * @since 3.0.1
 * @since 4.0.0 Use WP core `wp_doing_ajax()`.
 *
 * @return bool
 */
function llms_is_ajax() {
	return wp_doing_ajax();
}

/**
 * Determine if request is a REST request
 *
 * @since 3.27.0
 *
 * @return bool
 */
function llms_is_rest() {
	return ( defined( 'REST_REQUEST' ) && REST_REQUEST );
}

/**
 * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'.
 *
 * @thanks woocommerce <3.
 *
 * @since 3.0.0
 *
 * @return bool
 */
function llms_is_site_https() {
	return false !== strstr( get_option( 'home' ), 'https:' );
}

/**
 * Create an array that can be passed to metabox select elements configured as an llms-select2-post query-ier
 *
 * @since 3.0.0
 * @since 3.6.0 Unknown
 *
 * @param array  $post_ids  Optional. Indexed array of WordPress Post IDs. Defayult is empty array.
 * @param string $template  Optional. A template to customize the way the results look. Default is empty string.
 *                          {title} and {id} can be passed into the template
 *                          and will be replaced with the post title and post id respectively.
 * @return array
 */
function llms_make_select2_post_array( $post_ids = array(), $template = '' ) {

	if ( ! $template ) {
		$template = '{title} (' . __( 'ID#', 'lifterlms' ) . ' {id})';
	}

	if ( ! is_array( $post_ids ) ) {
		$post_ids = array( $post_ids );
	}

	$ret = array();
	foreach ( $post_ids as $id ) {

		$title = str_replace( array( '{title}', '{id}' ), array( get_the_title( $id ), $id ), $template );

		$ret[] = array(
			'key'   => $id,
			'title' => $title,
		);
	}
	/**
	 * Filter the select2 post array
	 *
	 * @since Unknown
	 *
	 * @param array Associative array of representing select2 post elements.
	 * @param array $post_ids  Optional. Indexed array of WordPress Post IDs.
	 */
	return apply_filters( 'llms_make_select2_post_array', $ret, $post_ids );

}

/**
 * Create an array that can be passed to metabox select elements configured as an llms-select2-student query-ier.
 *
 * @since 3.10.1
 * @version 3.23.0
 *
 * @param array  $user_ids Optional. Indexed array of WordPress User IDs. Default is empty array.
 * @param string $template Optional. A template to customize the way the results look. Default is empty string.
 *                         %1$s = student name
 *                         %2$s = student email.
 * @return array
 */
function llms_make_select2_student_array( $user_ids = array(), $template = '' ) {
	if ( ! $template ) {
		$template = '%1$s &lt;%2$s&gt;';
	}
	if ( ! is_array( $user_ids ) ) {
		$user_ids = array( $user_ids );
	}
	$ret = array();
	foreach ( $user_ids as $id ) {
		$student = llms_get_student( $id );
		if ( ! $student ) {
			continue;
		}
		$ret[] = array(
			'key'   => $id,
			'title' => sprintf( $template, $student->get_name(), $student->get( 'user_email' ) ),
		);
	}

	/**
	 * Filter the select2 student array
	 *
	 * @since Unknown
	 *
	 * @param array $elements  Associative array representing select2 student elements.
	 * @param array $post_ids  Optional. Indexed array of WordPress Post IDs.
	 */
	return apply_filters( 'llms_make_select2_student_array', $ret, $user_ids );
}

/**
 * Define a constant if it's not already defined
 *
 * @since 3.15.0
 *
 * @param string $name  Constant name.
 * @param mixed  $value Constant values.
 * @return void
 */
function llms_maybe_define_constant( $name, $value ) {
	if ( ! defined( $name ) ) {
		define( $name, $value );
	}
}

/**
 * Parse booleans
 *
 * Mostly used to parse yes/no bools stored in various meta data fields
 *
 * @since 3.16.0
 *
 * @param mixed $val Value to parse.
 * @return bool
 */
function llms_parse_bool( $val ) {
	return filter_var( $val, FILTER_VALIDATE_BOOLEAN );
}

/**
 * Convert a PHP error constant to a human readable error code
 *
 * @since 4.9.0
 *
 * @link https://www.php.net/manual/en/errorfunc.constants.php
 *
 * @param int $code A predefined php error constant.
 * @return string A human readable string version of the constant.
 */
function llms_php_error_constant_to_code( $code ) {

	$codes = array(
		E_ERROR             => 'E_ERROR', // 1.
		E_WARNING           => 'E_WARNING', // 2.
		E_PARSE             => 'E_PARSE', // 4.
		E_NOTICE            => 'E_NOTICE', // 8.
		E_CORE_ERROR        => 'E_CORE_ERROR', // 16.
		E_CORE_WARNING      => 'E_CORE_WARNING', // 32.
		E_COMPILE_ERROR     => 'E_COMPILE_ERROR', // 64.
		E_COMPILE_WARNING   => 'E_COMPILE_WARNING', // 128.
		E_USER_ERROR        => 'E_USER_ERROR', // 256.
		E_USER_WARNING      => 'E_USER_WARNING', // 512.
		E_USER_NOTICE       => 'E_USER_NOTICE', // 1024.
		E_STRICT            => 'E_STRICT', // 2048.
		E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', // 4096.
		E_DEPRECATED        => 'E_DEPRECATED', // 8192.
		E_USER_DEPRECATED   => 'E_USER_DEPRECATED', // 16384.
	);

	return isset( $codes[ $code ] ) ? $codes[ $code ] : $code;

}

/**
 * Redirect and exit
 *
 * Wrapper for WP core redirects which automatically calls `exit();`
 * and is pluggable (mainly for unit testing purposes).
 *
 * @since 3.19.4
 *
 * @param string $location Full URL to redirect to
 * @param array  $options  Optional. Array of options. Default is empty array.
 *                         $status int  HTTP status code of the redirect [default: 302].
 *                         $safe   bool If true, use `wp_safe_redirect()` otherwise use `wp_redirect()` [default: true],
 * @return void
 */
if ( ! function_exists( 'llms_redirect_and_exit' ) ) {
	function llms_redirect_and_exit( $location, $options = array() ) {

		$options = wp_parse_args(
			$options,
			array(
				'status' => 302,
				'safe'   => true,
			)
		);

		$func = $options['safe'] ? 'wp_safe_redirect' : 'wp_redirect';
		call_user_func( $func, $location, $options['status'] );
		exit();

	}
}

if ( ! function_exists( 'llms_setcookie' ) ) {
	/**
	 * Set a cookie.
	 *
	 * A pluggable wrapper for the native PHP function `set_cookie()`.
	 *
	 * The lifterlms-tests library plugs this function during unit testing so we can mock
	 * the returns of methods that set cookies and write tests for those functions.
	 *
	 * @since 4.0.0
	 *
	 * @link https://www.php.net/manual/en/function.setcookie.php
	 *
	 * @param string $name     The name of the cookie.
	 * @param string $value    The value of the cookie.
	 * @param int    $expires  The time wehn the cookie expires as a Unix timestamp.
	 * @param string $path     The path on the server where the cookie will be available.
	 * @param string $domain   The (sub)domain that the cookie is available to.
	 * @param bool   $secure   Indicates the cookie should only be transmitted over a secure HTTPS connection.
	 * @param bool   $httponly When `true` the cookie will only be made accessible through the HTTP protocol,
	 *                         preventing it from being accessed by scripting languages (such as Javascript).
	 *
	 * @return boolean
	 */
	function llms_setcookie( $name, $value = '', $expires = 0, $path = '', $domain = '', $secure = false, $httponly = false ) {
		return setcookie( $name, $value, $expires, $path, $domain, $secure, $httponly );
	}
}


/**
 * Wrapper for set_time_limit to ensure it's enabled before calling
 *
 * @since 3.16.5
 *
 * @source thanks WooCommerce <3
 *
 * @param int $limit  Optional. Script time limit. Default is 0 = no time limit.
 * @return void
 */
function llms_set_time_limit( $limit = 0 ) {

	if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {

		@set_time_limit( $limit ); // @phpcs:ignore

	}

}

/**
 * Trim a string and append a suffix
 *
 * @since 3.0.0
 *
 * @source thank you WooCommerce <3
 *
 * @param string $string Input string.
 * @param int    $chars  Optional. Max number of characters. Default is 200.
 * @param string $suffix Optional. A suffix to append. Default is '...'.
 * @return string
 */
function llms_trim_string( $string, $chars = 200, $suffix = '...' ) {
	if ( strlen( $string ) > $chars ) {
		if ( function_exists( 'mb_substr' ) ) {
			$string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix;
		} else {
			$string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix;
		}
	}
	return $string;
}

/**
 * Verify nonce with additional checks to confirm request method
 *
 * Skips verification if the nonce is not set
 * Useful for checking nonce for various LifterLMS forms which check for the form submission on init actions.
 *
 * @since 3.8.0
 * @since 3.35.0 Sanitize nonce field before verification.
 *
 * @param string $nonce          Name of the nonce field.
 * @param string $action         Name of the action.
 * @param string $request_method Optional. Name of the intended request method. Default is 'POST'.
 * @return null|false|int
 */
function llms_verify_nonce( $nonce, $action, $request_method = 'POST' ) {

	if ( strtoupper( getenv( 'REQUEST_METHOD' ) ) !== $request_method ) {
		return;
	}

	if ( empty( $_REQUEST[ $nonce ] ) ) {
		return;
	}

	return wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $nonce ] ) ), $action );

}
