<?php
/**
 * Handles assignment uploads and includes helper functions for assignments
 *
 * @since 2.1.0
 *
 * @package LearnDash\Assignments
 */

use LearnDash\Core\API;
use LearnDash\Core\Infrastructure\File_Protection\File_Download_Handler;
use LearnDash\Core\Utilities\Cast;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// cspell:ignore hasassignments .

/**
 * Filters the assignment post content.
 *
 * @since 4.10.3
 */
add_filter(
	'the_content',
	function( $content ): string {
		global $post;

		if (
			! $post instanceof WP_Post
			|| learndash_get_post_type_slug( LDLMS_Post_Types::ASSIGNMENT ) !== $post->post_type
		) {
			return $content;
		}

		return sprintf(
			'<p><a href="%s" target="_blank">%s</a></p>',
			esc_url( learndash_assignment_get_download_url( $post->ID ) ),
			esc_html__( 'Assignment file', 'learndash' )
		);
	}
);

/**
 * Handles the upload, delete, and mark as complete for the assignment.
 *
 * Fires on `parse_request` hook.
 *
 * @since 2.1.0
 */
function learndash_assignment_process_init() {

	if ( ( isset( $_POST['uploadfile'] ) ) && ( ! empty( $_POST['uploadfile'] ) ) && ( isset( $_POST['post'] ) ) && ( ! empty( $_POST['post'] ) ) && ( isset( $_POST['course_id'] ) && ( ! empty( $_POST['course_id'] ) ) ) ) {
		$course_id = intval( $_POST['course_id'] );
		$post_id   = intval( $_POST['post'] );

		// 1. Verify nonce
		if ( ! wp_verify_nonce( $_POST['uploadfile'], 'uploadfile_' . get_current_user_id() . '_' . $post_id ) ) {
			return;
		}

		// 2. Verify lesson/topic is set to accept assignment uploads. The 'lesson_assignment_upload'
		// should return 'on' if assignment uploads are enabled
		if ( 'on' !== learndash_get_setting( $post_id, 'lesson_assignment_upload' ) ) {
			return;
		}

		// 3. Verify the lesson/topic is from the correct course.
		$courses = learndash_get_courses_for_step( $post_id, true );
		if ( ( empty( $courses ) ) || ( ! isset( $courses[ $course_id ] ) ) ) {
			return;
		}

		// 4. Verify the user is logged in or allow external filtering
		if ( ! is_user_logged_in() ) {
			/**
			 * Filters whether to allow assignment for non logged in users.
			 *
			 * @param boolean $allow_upload Whether to allow assignment upload for non logged in users.
			 * @param int     $course_id    Course ID.
			 * @param int     $post_id      Post ID.
			 */
			if ( ! apply_filters( 'learndash_assignment_upload_user_check', false, $course_id, $post_id ) ) {
				return;
			}
		}

		$file = $_FILES['uploadfiles'];

		if ( ( ! empty( $file['name'][0] ) ) && ( learndash_check_upload( $file, $post_id ) ) ) {
			learndash_fileupload_process( $file, $post_id );
		}
	}

	if ( ! empty( $_GET['learndash_delete_attachment'] ) ) {
		$assignment_post = get_post( intval( $_GET['learndash_delete_attachment'] ) );
		if ( ( isset( $assignment_post ) ) && ( $assignment_post instanceof WP_Post ) && ( learndash_get_post_type_slug( 'assignment' ) === $assignment_post->post_type ) ) {
			$current_user_id = get_current_user_id();

			if ( ( $assignment_post->post_author == $current_user_id ) || ( learndash_is_admin_user( $current_user_id ) ) || ( learndash_is_group_leader_of_user( $current_user_id, $assignment_post->post_author ) ) ) {

				$course_id = get_post_meta( $assignment_post->ID, 'course_id', true );
				if ( empty( $course_id ) ) {
					$course_id = learndash_get_course_id( $assignment_post->ID );
				}
				$course_step_id = get_post_meta( $assignment_post->ID, 'lesson_id', true );

				learndash_process_mark_incomplete( $current_user_id, $course_id, $course_step_id );

				/**
				 * Filters whether to force delete the assignment or not.
				 *
				 * @param boolean $force_delete    Whether to force delete assignment or not.
				 * @param int     $assignment_id   Assignment ID.
				 * @param WP_POST $assignment_post Assignment post object.
				 */
				wp_delete_post( $assignment_post->ID, apply_filters( 'learndash_assignment_force_delete', true, $assignment_post->ID, $assignment_post ) );

				update_user_meta(
					get_current_user_id(),
					'ld_assignment_message',
					array(
						array(
							'type'    => 'success',
							'message' => esc_html__( 'Assignment successfully deleted.', 'learndash' ),
						),
					)
				);

				$return_url = remove_query_arg( 'learndash_delete_attachment' );
				learndash_safe_redirect( $return_url );
			}
		}
	}

	if ( ! empty( $_POST['attachment_mark_complete'] ) && ! empty( $_POST['userid'] ) ) {
		$lesson_id       = $_POST['attachment_mark_complete'];
		$current_user_id = get_current_user_id();
		$user_id         = $_POST['userid'];

		if ( ( learndash_is_admin_user( $current_user_id ) ) || ( learndash_is_group_leader_of_user( $current_user_id, $user_id ) ) ) {
			learndash_approve_assignment( $user_id, $lesson_id );
		}
	}
}

add_action( 'parse_request', 'learndash_assignment_process_init', 1 );

/**
 * Gets a list of user's assignments.
 *
 * @since 2.1.0
 * @since 4.5.0 Added optional param $fields.
 *
 * @param int    $post_id   Lesson ID.
 * @param int    $user_id   User ID.
 * @param int    $course_id Optional. Course ID. Default 0.
 * @param string $fields    Optional. Return array of assignment post IDs if set to 'ids'. Default 'all'.
 *
 * @return array Array of post objects or post IDs.
 */
function learndash_get_user_assignments( $post_id, $user_id, $course_id = 0, string $fields = 'all' ) {
	if ( empty( $course_id ) ) {
		$course_id = learndash_get_course_id( $post_id );
	}

	$opt = array(
		'post_type'      => 'sfwd-assignment',
		'posts_per_page' => - 1,
		'author'         => $user_id,
		'meta_query'     => array(
			'relation' => 'AND',
			array(
				'key'     => 'lesson_id',
				'value'   => $post_id,
				'compare' => '=',
			),
			array(
				'key'     => 'course_id',
				'value'   => $course_id,
				'compare' => '=',
			),
		),
		'fields'         => $fields,
	);
	return get_posts( $opt );
}

/**
 * Handles an assignment uploading process after the file was loaded.
 *
 * Updates assignment post meta with assignment data, creates a new assignment post, redirects to the needed page.
 *
 * @since 2.1.0
 * @since 4.10.3 Assignments have a learndash_version meta field to help distinguish between old and new assignments.
 *            Added the `$file_uploaded_name` parameter.
 * @since 4.24.0 Added the `uploaded_file_name` meta field. The assignment post titles are based on the originally uploaded file names only now, they used to be "Assignment {filename}".
 *
 * @param int    $post_id            Post ID.
 * @param string $fname              Assignment file name.
 * @param string $file_uploaded_name Assignment file uploaded name. Default empty. Optional.
 *
 * @return void
 */
function learndash_upload_assignment_init( $post_id, $fname, string $file_uploaded_name = '' ) {
	// Initialize an empty array.
	global $wp;

	if ( ! function_exists( 'wp_get_current_user' ) ) {
		include ABSPATH . 'wp-includes/pluggable.php';
	}

	$new_assignment_meta = array();
	$current_user        = wp_get_current_user();
	$username            = $current_user->user_login;
	$display_name        = $current_user->display_name;
	$url_link_arr        = wp_upload_dir();
	$url_link            = $url_link_arr['baseurl'];
	$dir_link            = $url_link_arr['basedir'];
	$file_path           = $dir_link . DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'assignments' . DIRECTORY_SEPARATOR;
	$url_path            = $url_link . DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'assignments' . DIRECTORY_SEPARATOR . $fname;

	if ( file_exists( $file_path . $fname ) ) {
		$dest = $url_path;
	} else {
		return;
	}

	update_post_meta( $post_id, 'sfwd_lessons-assignment', $new_assignment_meta );
	$post      = get_post( $post_id );
	$course_id = learndash_get_course_id( $post->ID );

	// It's not good to store the whole absolute URL and the path in the DB (file_link, file_path),
	// but I'm not touching it for now to keep the backward compatibility.
	// Once we have a chance to refactor the whole assignment upload process, we should change it.

	$assignment_meta = array(
		'course_id'          => $course_id,
		'disp_name'          => $display_name,                         // cspell:disable-line.
		'file_link'          => $dest,
		'file_name'          => $fname,
		'file_path'          => rawurlencode( $file_path . $fname ),
		'learndash_version'  => LEARNDASH_VERSION,
		'lesson_id'          => $post->ID,
		'lesson_title'       => $post->post_title,
		'lesson_type'        => $post->post_type,
		'uploaded_file_name' => $file_uploaded_name, // Added in v4.24.0.
		'user_id'            => $current_user->ID,
		'user_name'          => $username,
	);

	$points_enabled = learndash_get_setting( $post, 'lesson_assignment_points_enabled' );

	if ( 'on' === $points_enabled ) {
		$assignment_meta['points'] = 'pending';
	}

	$assignment = array(
		'post_title'   => $file_uploaded_name,
		'post_type'    => learndash_get_post_type_slug( 'assignment' ),
		'post_status'  => 'publish',
		'post_content' => '', // The content is added dynamically.
		'post_author'  => $current_user->ID,
	);

	$assignment_post_id = wp_insert_post( $assignment );
	$auto_approve       = learndash_get_setting( $post, 'auto_approve_assignment' );

	if ( $assignment_post_id ) {
		foreach ( $assignment_meta as $key => $value ) {
			update_post_meta( $assignment_post_id, $key, $value );
		}

		/**
		 * Fires after the assignment is uploaded.
		 *
		 * @since 2.2.0
		 *
		 * @param int   $assignment_post_id The assignment post id created after the assignment upload.
		 * @param array $assignment_meta    Assignment meta data.
		 */
		do_action( 'learndash_assignment_uploaded', $assignment_post_id, $assignment_meta );

		if ( empty( $auto_approve ) ) {

			update_user_meta(
				get_current_user_id(),
				'ld_assignment_message',
				array(
					array(
						'type'    => 'success',
						'message' => esc_html__( 'Assignment successfully uploaded.', 'learndash' ),
					),
				)
			);

			learndash_safe_redirect( get_permalink( $post->ID ), 303 );
		}
	}

	if ( ! empty( $auto_approve ) ) {
		learndash_approve_assignment( $current_user->ID, $post_id, $assignment_post_id );

		// assign full points if auto approve & points are enabled.
		if ( 'on' === $points_enabled ) {
			$points = learndash_get_setting( $post, 'lesson_assignment_points_amount' );
			update_post_meta( $assignment_post_id, 'points', intval( $points ) );
		}

		$redirect_to = learndash_course_get_step_completion_url(
			$post_id,
			Cast::to_int( $course_id ),
			$current_user->ID
		);

		learndash_safe_redirect( $redirect_to );
	}
}

/**
 * Handles whether the comments should be open for assignments.
 *
 * Fires on `comments_open` hook.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @since 2.1.0
 *
 * @param boolean $open    Whether the current post is open for comments.
 * @param int     $post_id The post ID.
 *
 * @return boolean True if the comments should be open otherwise false.
 */
function learndash_assignments_comments_open( $open, $post_id ) {
	if ( learndash_get_post_type_slug( 'assignment' ) === get_post_type( $post_id ) ) {
		$comment_status = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Assignments_CPT', 'comment_status' );
		if ( 'yes' === $comment_status ) {

			if ( empty( $open ) ) {
				if ( is_numeric( $post_id ) ) {
					global $wpdb;
					$wpdb->query(
						$wpdb->prepare(
							"UPDATE $wpdb->posts SET comment_status = %s WHERE ID = %d",
							'open',
							$post_id
						)
					);
					$open = true;
				}
			}
		} else {
			$open = false;
		}
	}

	return $open;
}

add_filter( 'comments_open', 'learndash_assignments_comments_open', 10, 2 );

/**
 * Enables comments when adding a new assignment.
 *
 * Fires on `wp_insert_post_data` hook.
 *
 * @since 2.1.0
 *
 * @param array $data An array of slashed post data.
 *
 * @return array $data post data
 */
function learndash_assignments_comments_on( $data ) {
	if ( learndash_get_post_type_slug( 'assignment' ) === $data['post_type'] ) {
		$data['comment_status'] = 'open';
	}

	return $data;
}
add_filter( 'wp_insert_post_data', 'learndash_assignments_comments_on' );

/**
 * Cleans file name on upload.
 *
 * @since 2.1.0
 *
 * @param string $string Name of the file.
 *
 * @return string Returns filename after cleaning.
 */
function learndash_clean_filename( $string ) {
	$string = htmlentities( $string, ENT_QUOTES, 'UTF-8' );
	$string = preg_replace( '~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i', '$1', $string );
	$string = html_entity_decode( $string, ENT_QUOTES, 'UTF-8' );
	$string = preg_replace( array( '~[^0-9a-z.]~i', '~[ -]+~' ), ' ', $string );
	$string = str_replace( ' ', '_', $string );
	return trim( $string, ' -' );
}

/**
 * Handles an assignment file uploading process.
 *
 * @since 2.1.0
 * @since 4.10.3 New files are created with a unique ID and in the /uploads/learndash/assignments/ directory.
 *
 * @param array $uploadfiles An array of uploaded files data.
 * @param int   $post_id    The assignment ID.
 *
 * @return array Returns file data after upload such as file name and file URL.
 */
function learndash_fileupload_process( $uploadfiles, $post_id ) {

	if ( is_array( $uploadfiles ) ) {

		foreach ( $uploadfiles['name'] as $key => $value ) {
			// look only for uploaded files.
			if ( 0 == $uploadfiles['error'][ $key ] ) {

				$file_tmp = $uploadfiles['tmp_name'][ $key ];

				$original_uploaded_filename = sanitize_text_field( $uploadfiles['name'][ $key ] );

				// clean filename.
				$filename = learndash_clean_filename( $uploadfiles['name'][ $key ] );

				// extract extension.
				if ( ! function_exists( 'wp_get_current_user' ) ) {
					include ABSPATH . 'wp-includes/pluggable.php';
				}

				// Before this function we have already validated the file extension/type via the function learndash_check_upload
				// @2.5.4.
				$file_uploaded_name = pathinfo( basename( $filename ), PATHINFO_FILENAME );
				$file_ext           = pathinfo( basename( $filename ), PATHINFO_EXTENSION );

				$upload_dir      = wp_upload_dir();
				$upload_dir_base = str_replace( '\\', DIRECTORY_SEPARATOR, $upload_dir['basedir'] );
				$upload_url_base = $upload_dir['baseurl'];
				$upload_dir_path = $upload_dir_base . DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'assignments';
				$upload_url_path = $upload_url_base . DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'assignments' . DIRECTORY_SEPARATOR;

				if ( ! file_exists( $upload_dir_path ) ) {
					if ( is_writable( dirname( $upload_dir_path ) ) ) {
						wp_mkdir_p( $upload_dir_path );
					} else {
						die( esc_html__( 'Unable to write to UPLOADS directory. Is this directory writable by the server?', 'learndash' ) );
					}
				}

				$file_time = microtime( true ) * 100;
				$filename  = sprintf( 'assignment_%d_%d_%s_%s.%s', $post_id, $file_time, $file_uploaded_name, uniqid(), $file_ext );

				/**
				 * Filters the assignment upload filename.
				 *
				 * @since 3.2.0
				 *
				 * @param string $filename   File name.
				 * @param int    $post_id    Post ID.
				 * @param float  $file_time  Unix timestamp.
				 * @param string $file_title Title of the file.
				 * @param string $file_ext   File extension.
				 */
				$filename = apply_filters(
					'learndash_assignment_upload_filename',
					$filename,
					$post_id,
					$file_time,
					$file_uploaded_name,
					$file_ext
				);

				/**
				 * Check if the filename already exist in the directory and rename the
				 * file if necessary
				 */
				$i = 0;

				$file_current_name = pathinfo( basename( $filename ), PATHINFO_FILENAME );
				$file_ext          = pathinfo( basename( $filename ), PATHINFO_EXTENSION );

				while ( file_exists( $upload_dir_path . '/' . $filename ) ) {
					$i++;
					$filename = $file_current_name . '_' . $i . '.' . $file_ext;
				}

				$file_dest   = $upload_dir_path . '/' . $filename;
				$destination = $upload_url_path . $filename;

				/**
				 * Check write permissions
				 */
				if ( ! is_writeable( $upload_dir_path ) ) {
					die( esc_html__( 'Unable to write to directory. Is this directory writable by the server?', 'learndash' ) );
				}

				/**
				 * Save temporary file to uploads dir
				 */
				if ( ! @move_uploaded_file( $file_tmp, $file_dest ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Better not to touch it for now.
					echo sprintf(
						// translators: placeholder: temporary file, file destination.
						esc_html__( 'Error, the file %1$s could not be moved to: %2$s', 'learndash' ),
						esc_html( $file_tmp ),
						esc_html( $file_dest )
					);
					continue;
				}

				/**
				 * Add upload meta to database
				 */
				learndash_upload_assignment_init( $post_id, $filename, $original_uploaded_filename );

				$file_desc             = array();
				$file_desc['filename'] = $filename;
				$file_desc['filelink'] = $destination;
				return $file_desc;
			}
		}
	}

	return array();
}

/**
 * Returns whether a lesson/topic supports assignment uploads.
 * Warning: It's a candidate for deprecation, use `Has_Assignments::supports_assignments()` instead.
 *
 * @since 2.1.0
 *
 * @param WP_Post $post The post object.
 *
 * @return bool
 */
function learndash_lesson_hasassignments( $post ) {
	$post_id     = $post->ID;
	$assign_meta = get_post_meta( $post_id, '_' . $post->post_type, true );

	if ( ! empty( $assign_meta[ $post->post_type . '_lesson_assignment_upload' ] ) ) {
		$val = $assign_meta[ $post->post_type . '_lesson_assignment_upload' ];

		if ( 'on' === $val ) {
			return true;
		} else {
			return false;
		}
	} else {
		return false;
	}
}

/**
 * Marks assignment approved by assignment ID.
 *
 * @since 2.1.0
 *
 * @param int $assignment_id Assignment ID.
 *
 * @return boolean Returns true if the assignment is approved otherwise false.
 */
function learndash_approve_assignment_by_id( $assignment_id ) {
	$assignment_post = get_post( $assignment_id );
	$user_id         = $assignment_post->post_author;
	$lesson_id       = get_post_meta( $assignment_post->ID, 'lesson_id', true );
	return learndash_approve_assignment( $user_id, $lesson_id, $assignment_id );
}



/**
 * Marks assignment approved by user ID and lesson ID.
 *
 * @since 2.1.0
 *
 * @param int $user_id            User ID.
 * @param int $lesson_id          Lesson ID.
 * @param int $assignment_post_id Optional. Assignment post ID. Default 0.
 *
 * @return bool Returns true if the assignment is approved, otherwise false.
 */
function learndash_approve_assignment( $user_id, $lesson_id, $assignment_post_id = 0 ) {
	// Prepare parameters.

	$user_id            = Cast::to_int( $user_id );
	$lesson_id          = Cast::to_int( $lesson_id );
	$assignment_post_id = Cast::to_int( $assignment_post_id );

	/**
	 * Filters whether an assignment should be approved or not.
	 *
	 * @since 2.1.0
	 * @since 2.5.5 Added `$assignment_post_id` parameter.
	 *
	 * @param boolean $approve            Whether assignment should be approved or not.
	 * @param int     $user_id            User ID.
	 * @param int     $lesson_id          Lesson ID.
	 * @param int     $assignment_post_id Assignment ID.
	 */
	$learndash_approve_assignment = apply_filters( 'learndash_approve_assignment', true, $user_id, $lesson_id, $assignment_post_id );

	if ( ! $learndash_approve_assignment ) {
		return false;
	}

	// This query needs to be reworked to NOT query all posts with that meta_key. Better off using WP_Query.
	global $wpdb;
	$assignment_ids = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %d",
			'lesson_id',
			$lesson_id
		)
	);

	foreach ( $assignment_ids as $assignment_id ) {
		if (
			$assignment_post_id !== 0
			&& $assignment_post_id !== Cast::to_int( $assignment_id )
		) {
			continue;
		}

		$assignment = get_post( $assignment_id );

		if (
			$assignment instanceof WP_Post
			&& Cast::to_int( $assignment->post_author ) === $user_id
		) {
			learndash_assignment_mark_approved( $assignment_id );

			/**
			 * Fires after assignment is approved
			 *
			 * @since 2.2.0
			 *
			 * @param int $assignment_id Assignment ID.
			 */
			do_action( 'learndash_assignment_approved', $assignment_id );
		}
	}

	return learndash_process_mark_complete(
		$user_id,
		$lesson_id,
		false,
		Cast::to_int( get_post_meta( $assignment_post_id, 'course_id', true ) )
	);
}



/**
 * Updates assignments post meta with approval status.
 *
 * @since 2.1.0
 *
 * @param int $assignment_id Assignment ID.
 */
function learndash_assignment_mark_approved( $assignment_id ) {
	update_post_meta( $assignment_id, 'approval_status', 1 );
}

/**
 * Gets assignments approval status.
 * Warning: It's a candidate for deprecation, use `Assignment::is_approved()` instead.
 *
 * @since 2.1.0
 *
 * @param int $assignment_id Assignment ID.
 *
 * @return int|false Status of assignment approval. Returns 1 if the assignment is approved.
 */
function learndash_is_assignment_approved_by_meta( $assignment_id ) {
	return get_post_meta( $assignment_id, 'approval_status', true );
}

/**
 * Checks if the assignment is approved or not.
 *
 * @since 2.1.0
 *
 * @param int $assignment_id Assignment ID.
 *
 * @return boolean|string Returns true if assignment approved otherwise false.
 */
function learndash_is_assignment_approved( $assignment_id ) {
	$assignment = get_post( $assignment_id );

	if ( empty( $assignment->ID ) ) {
		return '';
	}

	$lesson_id = learndash_get_lesson_id( $assignment->ID );

	if ( empty( $lesson_id ) ) {
		return '';
	}

	$lesson_completed = learndash_is_lesson_notcomplete( $assignment->post_author, array( $lesson_id => 1 ) );

	if ( empty( $lesson_completed ) ) {
		return true;
	} else {
		return false;
	}
}

/**
 * Manages the permissions to view the assignment.
 *
 * Only allow admins, group leaders, and assignment owners to see the assignment.
 * Fires on `wp` hook.
 *
 * @global WP_Post $post Global post object.
 *
 * @since 2.1.0
 */
function learndash_assignment_permissions() {
	if ( is_singular( learndash_get_post_type_slug( 'assignment' ) ) ) {

		$user_id = get_current_user_id();
		$post    = get_post();

		if ( learndash_is_admin_user( $user_id ) ) {
			return;
		} elseif ( learndash_is_group_leader_user( $user_id ) ) {
			/**
			 * For the Group Leader we check for common groups between
			 * Leader + Author + Course
			 */
			$course_id = get_post_meta( $post->ID, 'course_id', true );
			$course_id = absint( $course_id );

			if ( learndash_check_group_leader_course_user_intersect( $user_id, $post->post_author, $course_id ) ) {
				return;
			}
		} elseif ( absint( $user_id ) === absint( $post->post_author ) ) {
			return;
		}

		/**
		 * Filters Assignment permission redirect URL.
		 *
		 * @param string $redirect_url Redirect URL.
		 */
		learndash_safe_redirect( apply_filters( 'learndash_assignment_permissions_redirect_url', get_bloginfo( 'url' ) ) );
	} elseif ( ( is_home() ) || ( is_front_page() ) ) {
		/**
		 * Prevents the user from forcing the query on the home page
		 * with http://www.site.com?post_type=sfwd-assignment to access an archive.
		 *
		 * It would be nice if this is controllable via WP register_post_type() settings.
		 *
		 * See LEARNDASH-6390 for more details.
		 */
		if ( get_query_var( 'post_type', '' ) === learndash_get_post_type_slug( 'assignment' ) ) {
			// If this is an attempt we redirect them to the hme URL without the post_type query arg.
			$redirect_to_url = get_bloginfo( 'url' );
			if ( ! empty( $redirect_to_url ) ) {
				learndash_safe_redirect( $redirect_to_url );
			}
		}
	}
}

add_action( 'wp', 'learndash_assignment_permissions' );

/**
 * Registers assignments custom post type.
 *
 * Fires on `init` hook.
 *
 * @since 2.1.0
 */
function learndash_register_assignment_upload_type() {

	$exclude_from_search = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Assignments_CPT', 'exclude_from_search' );
	if ( 'yes' === $exclude_from_search ) {
		$exclude_from_search = true;
	} else {
		$exclude_from_search = false;
	}
	$publicly_queryable = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Assignments_CPT', 'publicly_queryable' );
	if ( 'yes' === $publicly_queryable ) {
		$publicly_queryable = true;
	} else {
		$publicly_queryable = false;
	}
	$comment_status = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Assignments_CPT', 'comment_status' );
	if ( 'yes' === $comment_status ) {
		$comment_status = true;
	} else {
		$comment_status = false;
	}

	$labels = array(
		'name'               => esc_html__( 'Assignments', 'learndash' ),
		'singular_name'      => esc_html__( 'Assignment', 'learndash' ),
		'edit_item'          => esc_html__( 'Edit Assignment', 'learndash' ),
		'view_item'          => esc_html__( 'View Assignment', 'learndash' ),
		'view_items'         => esc_html__( 'View Assignments', 'learndash' ),
		'search_items'       => esc_html__( 'Search Assignments', 'learndash' ),
		'not_found'          => esc_html__( 'No assignment found', 'learndash' ),
		'not_found_in_trash' => esc_html__( 'No assignment found in Trash', 'learndash' ),
		'parent_item_colon'  => esc_html__( 'Parent:', 'learndash' ),
		'menu_name'          => esc_html__( 'Assignments', 'learndash' ),
	);

	if ( learndash_is_admin_user() ) {
		$show_in_admin_bar = false;
	} elseif ( learndash_is_group_leader_user() ) {
		$show_in_admin_bar = false;
	} else {
		$show_in_admin_bar = false;
	}

	$supports = array( 'title', 'comments', 'author' );
	if ( true !== $comment_status ) {
		$supports = array_diff( $supports, array( 'comments' ) );
	}

	$rewrite = array( 'slug' => 'assignment' );
	if ( true !== $publicly_queryable ) {
		$rewrite = false;
	}

	$args = array(
		'labels'                => $labels,
		'hierarchical'          => false,
		'supports'              => $supports,
		'public'                => $publicly_queryable,
		'show_ui'               => true,
		'show_in_menu'          => true,
		'show_in_nav_menus'     => false,
		'show_in_admin_bar'     => $show_in_admin_bar,
		'publicly_queryable'    => $publicly_queryable,
		'exclude_from_search'   => $exclude_from_search,
		'has_archive'           => false,
		'show_in_rest'          => false,
		'rest_controller_class' => API\Controllers\Assignments::class,
		'query_var'             => $publicly_queryable,
		'rewrite'               => $rewrite,
		'capability_type'       => 'assignment',
		'capabilities'          => array(
			'read_post'              => 'read_assignment',
			'publish_posts'          => 'publish_assignments',
			'edit_posts'             => 'edit_assignments',
			'edit_others_posts'      => 'edit_others_assignments',
			'delete_posts'           => 'delete_assignments',
			'delete_others_posts'    => 'delete_others_assignments',
			'read_private_posts'     => 'read_private_assignments',
			'edit_post'              => 'edit_assignment',
			'delete_post'            => 'delete_assignment',
			'edit_published_posts'   => 'edit_published_assignments',
			'delete_published_posts' => 'delete_published_assignments',
			'create_posts'           => 'do_not_allow', // Remove the "Add New" button from the admin bar.
		),
		'map_meta_cap'          => true,
	);

	/**
	 * Filters the custom post type arguments.
	 *
	 * @param array  $cpt_args  Custom post type arguments.
	 * @param string $post_type Post type.
	 */
	$args = apply_filters( 'learndash-cpt-options', $args, 'sfwd-assignment' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores -- Better to keep it this way for now.

	register_post_type( 'sfwd-assignment', $args );
}

add_action( 'init', 'learndash_register_assignment_upload_type' );

/**
 * Setups capabilities for assignments custom post type.
 *
 * Fires on `admin_init` hook.
 *
 * @since 2.1.0
 *
 * @return void
 */
function learndash_init_assignments_capabilities(): void {
	$admin_role = get_role( 'administrator' );

	if ( $admin_role instanceof WP_Role ) {
		$admin_role_capabilities = array(
			'edit_assignment',
			'edit_assignments',
			'edit_others_assignments',
			'publish_assignments',
			'read_assignment',
			'read_private_assignments',
			'delete_assignment',
			'edit_published_assignments',
			'delete_others_assignments',
			'delete_published_assignments',
		);

		foreach ( $admin_role_capabilities as $capability ) {
			if ( ! $admin_role->has_cap( $capability ) ) {
				$admin_role->add_cap( $capability );
			}
		}
	}

	$group_leader_role = get_role( 'group_leader' );

	if ( $group_leader_role instanceof WP_Role ) {
		$group_leader_role_capabilities = array(
			'read_assignment',
			'edit_assignments',
			'edit_others_assignments',
			'edit_published_assignments',
			'delete_others_assignments',
			'delete_published_assignments',
		);

		foreach ( $group_leader_role_capabilities as $capability ) {
			if ( ! $group_leader_role->has_cap( $capability ) ) {
				$group_leader_role->add_cap( $capability );
			}
		}
	}
}

add_action( 'admin_init', 'learndash_init_assignments_capabilities' );

/**
 * Deletes assignment file when assignment post is deleted.
 *
 * Fires on `before_delete_post` hook.
 *
 * @since 2.1.0
 *
 * @param int $post_id Assignment post ID.
 */
function learndash_before_delete_assignment( $post_id ) {

	if ( ( ! empty( $post_id ) ) && ( learndash_get_post_type_slug( 'assignment' ) === get_post_type( $post_id ) ) ) {
		$file_path = get_post_meta( $post_id, 'file_path', true );
		if ( ! empty( $file_path ) ) {
			$file_path = rawurldecode( $file_path );

			if ( file_exists( $file_path ) ) {
				unlink( $file_path );
			}
		}
	}
}

add_action( 'before_delete_post', 'learndash_before_delete_assignment' );

/**
 * Returns the number of points awarded for an assignment.
 *
 * Displayed on single lessons under the submitted assignment
 *
 * @param int $assignment_id ID of the assignment.
 *
 * @return string Returns the number of points awarded string.
 */
function learndash_assignment_points_awarded( $assignment_id ) {
	$points_enabled = learndash_assignment_is_points_enabled( $assignment_id );

	if ( $points_enabled ) {
		$current = learndash_get_assignment_points_awarded( $assignment_id );

		/**
		 * Filters the output of the awarded points of an assignment.
		 *
		 * @param string $output  Output of the awarded points.
		 * @param string $current Points awarded values or translatable string.
		 */
		return apply_filters(
			'learndash_points_awarded_output',
			sprintf(
				// translators: placeholder: points awarded values (30/100) 30%.
				esc_html_x( 'Points Awarded: %s', 'placeholder: points awarded values (30/100) 30%', 'learndash' ),
				$current
			),
			$current
		);
	}

	return '';
}

/**
 * Gets the value of the awarded assignment points.
 *
 * If the assignment hasn't been approved or graded, the translatable string 'Pending' is returned.
 * Otherwise, the awarded points and percentage achieved are returned.
 *
 * @since 2.6.4
 *
 * @param int $assignment_id ID of the assignment.
 *
 * @return string Returns points awarded.
 */
function learndash_get_assignment_points_awarded( $assignment_id ) {
	$current = get_post_meta( $assignment_id, 'points', true );

	// We can't compare against the actual post meta value because it was a translatable string until 2.6.4.
	if ( ( ! empty( $current ) ) && ( ! is_numeric( $current ) ) ) {
		return esc_html__( 'Pending', 'learndash' );
	}

	if ( is_numeric( $current ) ) {
		$assignment_settings_id = intval( get_post_meta( $assignment_id, 'lesson_id', true ) );
		$max_points             = learndash_get_setting( $assignment_settings_id, 'lesson_assignment_points_amount' );
		$max_points             = intval( $max_points );
		if ( ! empty( $max_points ) ) {
			$percentage = ( intval( $current ) / intval( $max_points ) ) * 100;
			$percentage = round( $percentage, 2 );
		} else {
			$percentage = 0.00;
		}

		/**
		 * Filters the output format of the awarded points of an assignment.
		 *
		 * @param string $output_format Output Format of awarded points.
		 * @param string $current       Achieved points.
		 * @param int    $max_points    Maximum points.
		 * @param float  $percentage    Percentage of achieved points/maximum points.
		 */
		return apply_filters(
			'learndash_points_awarded_output_format',
			sprintf(
				'(%1$d/%2$d) %3$d&#37; ',
				$current,
				$max_points,
				$percentage
			),
			$current,
			$max_points,
			$percentage
		);
	}

	return '';
}

/**
 * Checks if the points are enabled for the assignment.
 *
 * Warning: This function is a candidate for deprecation. Use `Has_Assignments::has_assignment_points_enabled()` instead.
 *
 * @param int|WP_Post $assignment The assignment `WP_Post` object or ID.
 *
 * @return boolean Returns true if the points are enabled otherwise false.
 */
function learndash_assignment_is_points_enabled( $assignment ) {
	if ( is_a( $assignment, 'WP_Post' ) ) {
		$assignment_id = $assignment->ID;
	} else {
		$assignment_id = intval( $assignment );
	}

	$assignment_settings_id = intval( get_post_meta( $assignment_id, 'lesson_id', true ) );
	$points_enabled         = learndash_get_setting( $assignment_settings_id, 'lesson_assignment_points_enabled' );

	if ( 'on' === $points_enabled ) {
		return true;
	}

	return false;
}

/**
 * Converts the file size shorthand to bytes.
 *
 * @since 2.5.0
 * @since 4.24.0 Better support for 32bit systems.
 *
 * @param int|string $val Optional. Shorthand notation for file size like 1024M. Default 0.
 *
 * @return int Returns the bytes after converting from shorthand.
 */
function learndash_return_bytes_from_shorthand( $val = 0 ) {
	if ( is_numeric( $val ) ) {
		// Pure numeric input, assume it's already bytes.
		return Cast::to_int( (float) $val > PHP_INT_MAX ? PHP_INT_MAX : $val );
	}

	if (
		is_string( $val )
		&& preg_match( '/([\d\.]+)\s*([kmg]?b?)/i', $val, $matches ) // cspell:disable-line -- Regex.
	) {
		$units = [
			'K' => 1,
			'M' => 2,
			'G' => 3,
			'B' => 0,
		];

		$number = (float) $matches[1];
		$unit   = strtoupper( $matches[2] );

		// Normalize common shorthand (e.g., 'KB' vs 'K').
		$unit  = str_replace(
			[ 'KB', 'MB', 'GB' ],
			[ 'K', 'M', 'G' ],
			$unit
		);
		$power = $units[ $unit ] ?? 0;

		$bytes = $number * pow( 1024, $power );

		return Cast::to_int( (float) $bytes > PHP_INT_MAX ? PHP_INT_MAX : $bytes );
	}

	// Fallback: best-effort cast.
	return Cast::to_int( (float) $val > PHP_INT_MAX ? PHP_INT_MAX : $val );
}

/**
 * Checks whether the assignment upload is successful or not.
 *
 * @param array $uploadfiles An array of uploaded files data.
 * @param int   $post_id    Optional. The Assignment ID. Default 0.
 *
 * @return boolean
 */
function learndash_check_upload( $uploadfiles = array(), $post_id = 0 ) {
	$post_settings = array();

	if ( ( is_array( $uploadfiles ) ) && ( ! empty( $post_id ) ) ) {
		$limit_file_exts = array();
		$limit_file_size = 0;

		$post_settings = learndash_get_setting( $post_id );
		if ( ( isset( $post_settings['assignment_upload_limit_size'] ) ) && ( ! empty( $post_settings['assignment_upload_limit_size'] ) ) ) {
			$limit_file_size = $post_settings['assignment_upload_limit_size'];
			$limit_file_size = learndash_return_bytes_from_shorthand( $limit_file_size );

		} else {
			$limit_file_size = wp_max_upload_size();
		}

		if ( ( empty( $limit_file_size ) ) || ( intval( $uploadfiles['size'][0] ) > $limit_file_size ) ) {
			update_user_meta(
				get_current_user_id(),
				'ld_assignment_message',
				array(
					array(
						'type'    => 'error',
						'message' => esc_html__( 'Uploaded file size exceeds allowed limit.', 'learndash' ),
					),
				)
			);
			return false;
		}

		$limit_file_exts = learndash_get_allowed_upload_mime_extensions_for_post( $post_id );
		$filetype_mime   = wp_check_filetype( $uploadfiles['name'][0], $limit_file_exts );

		if ( ( empty( $filetype_mime ) ) || ( empty( $filetype_mime['ext'] ) ) || ( empty( $filetype_mime['type'] ) ) || ( ! $limit_file_exts[ strtolower( $filetype_mime['ext'] ) ] ) ) {
			update_user_meta(
				get_current_user_id(),
				'ld_assignment_message',
				array(
					array(
						'type'    => 'error',
						'message' => esc_html__( 'The uploaded file type is not allowed.', 'learndash' ),
					),
				)
			);
			return false;
		}

		if ( isset( $post_settings['assignment_upload_limit_count'] ) ) {
			$assignment_upload_limit_count = intval( $post_settings['assignment_upload_limit_count'] );
			if ( $assignment_upload_limit_count > 0 ) {
				$assignments = learndash_get_user_assignments( $post_id, get_current_user_id() );
				if ( ( ! empty( $assignments ) ) && ( count( $assignments ) >= $assignment_upload_limit_count ) ) {
					update_user_meta(
						get_current_user_id(),
						'ld_assignment_message',
						array(
							array(
								'type'    => 'error',
								'message' => esc_html__( 'Number of allowed assignment uploads reached.', 'learndash' ),
							),
						)
					);
					return false;
				}
			}
		}
	}

	/**
	 * Filter to allow external approval of Assignment upload.
	 *
	 * @since 3.4.0
	 *
	 * @param bool  $assignment_approved If Assignment has been approved. Default true.
	 * @param array $uploadfiles         Array of uploaded files.
	 * @param int   $post_id             Assignment Post ID.
	 * @param array $post_settings       Array of Assignment Post Settings.
	 *
	 * @return bool true if upload is approved. false if not.
	 *
	 * The external processor should set a usermeta entry 'ld_assignment_message' with the
	 * error message. This will be shown to the user.
	 *
	 * Example:
	 * update_user_meta(
	 *    get_current_user_id(),
	 *    'ld_assignment_message',
	 *    array(
	 *       array(
	 *          'type'    => 'error',
	 *          'message' => esc_html__( 'Number of allowed assignment uploads reached.', 'learndash' ),
	 *       ),
	 *    )
	 * );
	 */
	return (bool) apply_filters( 'learndash_assignment_check_upload', true, $uploadfiles, $post_id, $post_settings );
}

/**
 * Checks whether all assignments for specified step and user are approved.
 *
 * @since 4.5.0
 *
 * @param array<int> $assignment_ids An array of assignment IDs.
 * @param int        $step_id        The ID of the lesson or topic to get user assignments from.
 * @param int        $user_id        Optional. The user ID, gets current user if not specified.
 *
 * @return boolean True if all user assignments for step are approved, otherwise false.
 */
function learndash_assignment_list_approved( array $assignment_ids, int $step_id, int $user_id ): bool {
	if ( ! is_array( $assignment_ids ) ) {
		return false;
	}

	if ( empty( $user_id ) ) {
		$user_id = get_current_user_id();
	}

	if ( ! empty( $step_id ) ) {
		$course_id      = absint( learndash_get_course_id( $step_id ) );
		$assignment_ids = learndash_get_user_assignments( $step_id, $user_id, $course_id, 'ids' );
	}

	foreach ( $assignment_ids as $assignment ) {
		$approval_status = learndash_is_assignment_approved_by_meta( $assignment );

		if ( ! $approval_status ) {
			return false;
		}
	}

	return true;
}

/**
 * Returns the URL to download the assignment file.
 * Warning: It's a candidate for deprecation, use `Assignment::get_download_url()` instead.
 *
 * @since 4.10.3
 *
 * @param int $post_id Assignment ID.
 *
 * @return string
 */
function learndash_assignment_get_download_url( int $post_id ): string {
	$download_url = '';
	$file_name    = Cast::to_string(
		get_post_meta( $post_id, 'file_name', true )
	);

	if ( ! empty( $file_name ) ) {
		// Since LD version 4.10.3 we've changed the path ID and added the learndash_version meta.
		$file_path_id = get_post_meta( $post_id, 'learndash_version', true )
		? 'uploads_learndash_assignments'
		: 'uploads_assignments';

		try {
			$download_url = File_Download_Handler::get_download_url( $file_path_id, $file_name );
		} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Ignore.
		}
	}

	/**
	 * Filters the returned assignment download URL.
	 *
	 * @since 4.19.0
	 *
	 * @param string $download_url The URL to download the assignment file.
	 * @param int    $post_id      Assignment Post ID.
	 * @param string $file_name    Assignment file name.
	 *
	 * @return string The URL to download the essay file.
	 */
	return apply_filters(
		'learndash_assignment_get_download_url',
		$download_url,
		$post_id,
		$file_name
	);
}
