<?php
/**
* Adds ability to have "Essay / Open Answer" questions in Wp Pro Quiz
*
* @since 2.2.0
*
* @package LearnDash\Essay
*/
use LearnDash\Core\Infrastructure\File_Protection\File_Download_Handler;
use LearnDash\Core\Utilities\Cast;
use LearnDash\Core\API;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the essay post type.
*
* Holds the responses of essay questions submitted by the user.
* Fires on `init` hook.
*
* @since 2.2.0
*/
function learndash_register_essay_post_type() {
$labels = array(
'name' => esc_html_x( 'Submitted Essays', 'Post Type General Name', 'learndash' ),
'singular_name' => esc_html_x( 'Submitted Essay', 'Post Type Singular Name', 'learndash' ),
'menu_name' => esc_html__( 'Submitted Essays', 'learndash' ),
'name_admin_bar' => esc_html__( 'Submitted Essays', 'learndash' ),
'parent_item_colon' => esc_html__( 'Parent Submitted Essay:', 'learndash' ),
'all_items' => esc_html__( 'All Submitted Essays', 'learndash' ),
'add_new_item' => esc_html__( 'Add New Submitted Essay', 'learndash' ),
'add_new' => esc_html__( 'Add New', 'learndash' ),
'new_item' => esc_html__( 'New Submitted Essay', 'learndash' ),
'edit_item' => esc_html__( 'Edit Submitted Essay', 'learndash' ),
'update_item' => esc_html__( 'Update Submitted Essay', 'learndash' ),
'view_item' => esc_html__( 'View Submitted Essay', 'learndash' ),
'view_items' => esc_html__( 'View Submitted Essays', 'learndash' ),
'search_items' => esc_html__( 'Search Submitted Essays', 'learndash' ),
'not_found' => esc_html__( 'Submitted Essay Not found', 'learndash' ),
'not_found_in_trash' => esc_html__( 'Submitted Essay Not found in Trash', 'learndash' ),
'item_published' => esc_html__( 'Submitted Essay Published', 'learndash' ),
'item_published_privately' => esc_html__( 'Submitted Essay Published Privately', 'learndash' ),
'item_reverted_to_draft' => esc_html__( 'Submitted Essay Reverted to Draft', 'learndash' ),
'item_scheduled' => esc_html__( 'Submitted Essay Scheduled', 'learndash' ),
'item_updated' => esc_html__( 'Submitted Essay Updated', 'learndash' ),
);
$capabilities = array(
'edit_essay' => 'edit_essay',
'read_essay' => 'read_essay',
'delete_essay' => 'delete_essay',
'edit_essays' => 'edit_essays',
'edit_others_essays' => 'edit_others_essays',
'publish_essays' => 'publish_essays',
'read_private_essays' => 'read_private_essays',
);
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;
}
$args = array(
'label' => esc_html__( 'sfwd-essays', 'learndash' ),
// translators: quiz, question.
'description' => sprintf( esc_html_x( 'Submitted essays via a %1$s %2$s.', 'placeholder: quiz, question', 'learndash' ), learndash_get_custom_label_lower( 'quiz' ), learndash_get_custom_label_lower( 'question' ) ),
'labels' => $labels,
'supports' => array( 'title', 'editor', 'comments', 'author' ),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => false,
'show_in_admin_bar' => $show_in_admin_bar,
'query_var' => true,
'rewrite' => array( 'slug' => 'essay' ),
'menu_position' => 5,
'show_in_nav_menus' => false,
'can_export' => true,
'has_archive' => false,
'show_in_rest' => false,
'rest_controller_class' => API\Controllers\Essays::class,
'exclude_from_search' => true,
'publicly_queryable' => true,
'capability_type' => 'essay',
'capabilities' => $capabilities,
'map_meta_cap' => true,
);
/** This filter is documented in includes/ld-assignment-uploads.php */
$args = apply_filters( 'learndash-cpt-options', $args, 'sfwd-essays' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores -- Better to keep it this way for now.
register_post_type( 'sfwd-essays', $args );
}
add_action( 'init', 'learndash_register_essay_post_type' );
/**
* Adds the essay post type capabilities.
*
* Add essay capabilities to administrators and group leaders.
* Fires on `admin_init` hook.
*
* @since 2.2.0
*/
function learndash_add_essay_caps() {
$admin_role = get_role( 'administrator' );
if ( ( $admin_role ) && ( $admin_role instanceof WP_Role ) ) {
$cap = $admin_role->has_cap( 'delete_others_essays' );
if ( empty( $cap ) ) {
$admin_role->add_cap( 'edit_essays' );
$admin_role->add_cap( 'edit_others_essays' );
$admin_role->add_cap( 'publish_essays' );
$admin_role->add_cap( 'read_essays' );
$admin_role->add_cap( 'read_private_essays' );
$admin_role->add_cap( 'delete_essays' );
$admin_role->add_cap( 'edit_published_essays' );
$admin_role->add_cap( 'delete_others_essays' );
$admin_role->add_cap( 'delete_published_essays' );
}
}
$group_leader_role = get_role( 'group_leader' );
if ( ( $group_leader_role ) && ( $group_leader_role instanceof WP_Role ) ) {
$group_leader_role->add_cap( 'edit_essays' );
$group_leader_role->add_cap( 'edit_others_essays' );
$group_leader_role->add_cap( 'publish_essays' );
$group_leader_role->add_cap( 'read_essays' );
$group_leader_role->add_cap( 'read_private_essays' );
$group_leader_role->add_cap( 'delete_essays' );
$group_leader_role->add_cap( 'edit_published_essays' );
$group_leader_role->add_cap( 'delete_others_essays' );
$group_leader_role->add_cap( 'delete_published_essays' );
}
}
add_action( 'admin_init', 'learndash_add_essay_caps' );
/**
* Maps the meta capabilities for essay post type.
*
* Fires on `map_meta_cap` hook.
*
* @since 2.2.0
*
* @param array $caps An Array of the user's capabilities.
* @param string $cap Capability name.
* @param int $user_id The User ID.
* @param array $args Optional. Adds the context to the cap. Typically the object ID. Default empty array.
*
* @return array An array of user's capabilities.
*/
function learndash_map_metacap_essays( $caps, $cap, $user_id, $args = array() ) {
if ( ! is_string( $cap ) ) {
return $caps;
}
/* If editing, deleting, or reading a essays, get the post and post type object. */
if ( 'edit_essay' == $cap || 'delete_essay' == $cap || 'read_essay' == $cap ) {
// Ensure $args is valid.
if ( ( ! is_array( $args ) ) || ( ! isset( $args[0] ) ) ) {
return $caps;
}
$post = get_post( $args[0] );
if ( ! is_a( $post, 'WP_Post' ) ) {
return $caps;
}
$post_type = get_post_type_object( $post->post_type );
/* Set an empty array for the caps. */
$caps = array();
}
/* If editing a essay, assign the required capability. */
if ( 'edit_essay' == $cap ) {
if ( $user_id == $post->post_author ) {
$caps[] = $post_type->cap->edit_posts;
} else {
$caps[] = $post_type->cap->edit_others_posts;
}
} elseif ( 'delete_essay' == $cap ) { /* If deleting a essay, assign the required capability. */
if ( $user_id == $post->post_author ) {
$caps[] = $post_type->cap->delete_posts;
} else {
$caps[] = $post_type->cap->delete_others_posts;
}
} elseif ( 'read_essay' == $cap ) { /* If reading a private essay, assign the required capability. */
if ( 'private' != $post->post_status ) {
$caps[] = 'read';
} elseif ( $user_id == $post->post_author ) {
$caps[] = 'read';
} else {
$caps[] = $post_type->cap->read_private_posts;
}
}
/* Return the capabilities required by the user. */
return $caps;
}
add_filter( 'map_meta_cap', 'learndash_map_metacap_essays', 10, 4 );
/**
* Registers the 'Graded' and 'Not Graded' post status.
*
* Fires on `init` hook.
*
* @since 2.2.0
*/
function learndash_register_essay_post_status() {
register_post_status(
'graded',
array(
'label' => esc_html_x( 'Graded', 'Custom Essay post type status: Graded', 'learndash' ),
'public' => true,
'exclude_from_search' => true,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
// translators: placeholder: Graded Essay count.
'label_count' => _n_noop( 'Graded <span class="count">(%s)</span>', 'Graded <span class="count">(%s)</span>', 'learndash' ),
)
);
register_post_status(
'not_graded',
array(
'label' => esc_html_x( 'Not Graded', 'Custom Essay post type status: Not Graded', 'learndash' ),
'public' => true,
'exclude_from_search' => true,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
// translators: placeholder: Not Graded Essay count.
'label_count' => _n_noop( 'Not Graded <span class="count">(%s)</span>', 'Not Graded <span class="count">(%s)</span>', 'learndash' ),
)
);
}
add_action( 'init', 'learndash_register_essay_post_status' );
/**
* Manages the permissions for the essay post type.
*
* Only allow admins, group leaders, and essay owners to see the assignment.
* Fires on `wp` hook.
*
* @global WP_Post $post Global post object.
*
* @since 2.2.1
*/
function learndash_essay_permissions() {
if ( is_singular( learndash_get_post_type_slug( 'essay' ) ) ) {
$can_view_file = false;
$post = get_post();
if ( ( $post ) && ( is_a( $post, 'WP_Post' ) ) && ( learndash_get_post_type_slug( 'essay' ) === $post->post_type ) ) {
$user_id = get_current_user_id();
if ( ! empty( $user_id ) ) {
if ( ( learndash_is_admin_user( $user_id ) ) || ( $post->post_author == $user_id ) ) {
$can_view_file = true;
} 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 );
// For the future, if the essay is not associated with a course, we can't check for common groups.
// It feels like a bug, but I'm not fixing it at the moment.
if ( learndash_check_group_leader_course_user_intersect( $user_id, $post->post_author, $course_id ) ) {
$can_view_file = true;
}
}
}
}
if ( true === $can_view_file ) {
$uploaded_file = get_post_meta( $post->ID, 'upload', true );
if (
$post
&& ! empty( $uploaded_file )
&& ! strstr( $post->post_content, $uploaded_file )
) {
$quiz_essay_upload_link = '<p><a target="_blank" href="' . esc_url( learndash_quiz_essay_get_download_url( $post->ID ) ) . '">' . esc_html__( 'View uploaded file', 'learndash' ) . '</a></p>';
/**
* Filters quiz essay upload link HTML output.
*
* @deprecated 4.5.0
*
* @param string $upload_link Essay upload link HTML output.
*/
$quiz_essay_upload_link = apply_filters_deprecated(
'learndash-quiz-essay-upload-link',
array( $quiz_essay_upload_link ),
'4.5.0',
'learndash_quiz_essay_upload_link'
);
/**
* Filters quiz essay upload link HTML output.
*
* @since 4.5.0
*
* @param string $upload_link Essay upload link HTML output.
*/
$quiz_essay_upload_link = apply_filters( 'learndash_quiz_essay_upload_link', $quiz_essay_upload_link );
$post->post_content .= $quiz_essay_upload_link;
}
return;
} else {
if ( empty( $user_id ) ) {
$current_url = remove_query_arg( 'test' );
$redirect_to_url = wp_login_url( esc_url( $current_url ), true );
} else {
$redirect_to_url = get_bloginfo( 'url' );
}
/**
* Filters the URL to redirect a user if it does not have permission to view the essay.
*
* @param string $redirect_url Redirect URL.
*/
$redirect_to_url = apply_filters( 'learndash_essay_permissions_redirect_url', $redirect_to_url );
if ( ! empty( $redirect_to_url ) ) {
learndash_safe_redirect( $redirect_to_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-essays to access an archive.
*
* It would be nice if this is controllable via WP register_post_type() settings.
*
* See LEARNDASH-6389 for more details.
*/
if ( get_query_var( 'post_type', '' ) === learndash_get_post_type_slug( 'essay' ) ) {
// 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_essay_permissions' );
/**
* Adds a new essay response.
*
* Called from `LD_QuizPro::checkAnswers()` via AJAX.
*
* @since 2.2.0
* @since 4.10.3 Essays have a learndash_version meta field to help distinguish between old and new Essays.
*
* @param string $response Essay response.
* @param WpProQuiz_Model_Question $this_question Pro quiz question object.
* @param WpProQuiz_Model_Quiz $quiz Pro Quiz object.
* @param array|null $post_data Optional. Quiz information and answers. Default null.
*
* @return boolean|int|WP_Error Returns essay ID or `WP_Error` if the essay could not be created.
*/
function learndash_add_new_essay_response( $response, $this_question, $quiz, $post_data = null ) {
if ( ! is_a( $this_question, 'WpProQuiz_Model_Question' ) || ! is_a( $quiz, 'WpProQuiz_Model_Quiz' ) ) {
return false;
}
$user = wp_get_current_user();
// essay args defaults.
$essay_args = array(
'post_title' => $this_question->getTitle(),
'post_status' => 'draft',
'post_type' => 'sfwd-essays',
'post_author' => $user->ID,
);
$essay_data = $this_question->getAnswerData();
$essay_data = array_shift( $essay_data );
// switch on grading progression in order to set post status.
switch ( $essay_data->getGradingProgression() ) {
case '':
case 'not-graded-none':
$essay_args['post_status'] = 'not_graded';
break;
case 'not-graded-full':
$essay_args['post_status'] = 'not_graded';
break;
case 'graded-full':
$essay_args['post_status'] = 'graded';
break;
}
$essay_args['post_status'] = 'draft';
// switch on graded type to handle the response
// used a switch in case we add more types.
switch ( $essay_data->getGradedType() ) {
case 'text':
$essay_args['post_content'] = wp_kses(
$response,
/**
* Filters list of allowed html tags in essay content.
*
* Used in allowed_html parameter of `wp_kses` function.
*
* @param array $allowed_tags An array of allowed HTML tags in essay content.
*/
apply_filters( 'learndash_essay_new_allowed_html', wp_kses_allowed_html( 'post' ) )
);
break;
case 'upload':
$essay_args['post_content'] = esc_html__( 'See upload below.', 'learndash' );
}
/**
* Filters new essay submission `wp_insert_post` arguments.
*
* @param array $essay_args An array of essay arguments.
*/
$essay_args = apply_filters( 'learndash_new_essay_submission_args', $essay_args );
$essay_id = wp_insert_post( $essay_args );
if ( ! empty( $essay_id ) ) {
if ( ( isset( $post_data['quiz_id'] ) ) && ( ! empty( $post_data['quiz_id'] ) ) ) {
$quiz_id = absint( $post_data['quiz_id'] );
} else {
$quiz_id = learndash_get_quiz_id_by_pro_quiz_id( $this_question->getQuizId() );
}
if ( isset( $post_data['course_id'] ) ) {
$course_id = intval( $post_data['course_id'] );
if ( ! empty( $course_id ) ) {
$lesson_id = learndash_course_get_single_parent_step( $course_id, $quiz_id );
} else {
$lesson_id = 0;
}
} else {
$course_id = learndash_get_course_id( $quiz_id );
$lesson_id = learndash_get_lesson_id( $quiz_id );
}
update_post_meta( $essay_id, 'question_id', $this_question->getId() );
update_post_meta( $essay_id, 'quiz_pro_id', $this_question->getQuizId() );
update_post_meta( $essay_id, 'quiz_id', $this_question->getQuizId() );
update_post_meta( $essay_id, 'course_id', $course_id );
update_post_meta( $essay_id, 'lesson_id', $lesson_id );
update_post_meta( $essay_id, 'quiz_post_id', $quiz->getPostId() );
update_post_meta( $essay_id, 'question_post_id', $this_question->getQuestionPostId() );
update_post_meta( $essay_id, 'learndash_version', LEARNDASH_VERSION );
if ( 'upload' == $essay_data->getGradedType() ) {
update_post_meta( $essay_id, 'upload', esc_url( $response ) );
}
}
/**
* Fires after a new essay is submitted.
*
* @param int $essay_id The new Essay ID created after essay submission.
* @param array $essay_arg An array of essay arguments.
*/
do_action( 'learndash_new_essay_submitted', $essay_id, $essay_args );
return $essay_id;
}
/**
* Gets the essay data for this particular submission
*
* Loop through all the quizzes and return the quiz that matches as soon as it's found.
*
* @since 2.2.0
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param WP_Post $essay The `WP_Post` essay object.
*
* @return mixed The submitted essay data.
*/
function learndash_get_submitted_essay_data( $quiz_id, $question_id, $essay ) {
$users_quiz_data = get_user_meta( $essay->post_author, '_sfwd-quizzes', true );
if ( ( ! empty( $users_quiz_data ) ) && ( is_array( $users_quiz_data ) ) ) {
if ( ( $essay ) && ( is_a( $essay, 'WP_Post' ) ) ) {
$essay_quiz_time = get_post_meta( $essay->ID, 'quiz_time', true );
} else {
$essay_quiz_time = null;
}
foreach ( $users_quiz_data as $quiz_data ) {
// We check for a match on the quiz time from the essay postmeta first.
// If the essay_quiz_time is not empty and does NOT match then continue.
if ( ( absint( $essay_quiz_time ) ) && ( isset( $quiz_data['time'] ) ) && ( absint( $essay_quiz_time ) !== absint( $quiz_data['time'] ) ) ) {
continue;
}
if ( empty( $quiz_data['pro_quizid'] ) || $quiz_id != $quiz_data['pro_quizid'] || ! isset( $quiz_data['has_graded'] ) || false == $quiz_data['has_graded'] ) {
continue;
}
if ( ( isset( $quiz_data['graded'] ) ) && ( ! empty( $quiz_data['graded'] ) ) ) {
foreach ( $quiz_data['graded'] as $key => $graded_question ) {
if ( ( $key == $question_id ) && ( $essay->ID == $graded_question['post_id'] ) ) {
return $quiz_data['graded'][ $key ];
}
}
}
}
}
}
/**
* Updates the user's submitted essay data.
*
* Finds the essay in this particular quiz attempt in the user's meta and updates its data.
*
* @since 2.2.0
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param WP_Post $essay The `WP_Post` essay object.
* @param array $submitted_essay Submitted essay data.
*/
function learndash_update_submitted_essay_data( $quiz_id, $question_id, $essay, $submitted_essay ) {
$users_quiz_data = get_user_meta( $essay->post_author, '_sfwd-quizzes', true );
if ( ( $essay ) && ( is_a( $essay, 'WP_Post' ) ) ) {
$essay_quiz_time = get_post_meta( $essay->ID, 'quiz_time', true );
} else {
$essay_quiz_time = null;
}
$quizdata_changed = array();
foreach ( $users_quiz_data as $quiz_key => $quiz_data ) {
// We check for a match on the quiz time from the essay postmeta first.
// If the essay_quiz_time is not empty and does NOT match then continue.
if ( ( absint( $essay_quiz_time ) ) && ( isset( $quiz_data['time'] ) ) && ( absint( $essay_quiz_time ) !== absint( $quiz_data['time'] ) ) ) {
continue;
}
if ( $quiz_id != $quiz_data['pro_quizid'] || ! isset( $quiz_data['has_graded'] ) || false == $quiz_data['has_graded'] ) {
continue;
}
foreach ( $quiz_data['graded'] as $question_key => $graded_question ) {
if ( ( $question_key == $question_id ) && ( $essay->ID == $graded_question['post_id'] ) ) {
$users_quiz_data[ $quiz_key ]['graded'][ $question_key ] = $submitted_essay;
if ( ( isset( $submitted_essay['status'] ) ) && ( 'graded' === $submitted_essay['status'] ) ) {
$quizdata_changed[] = $users_quiz_data[ $quiz_key ];
}
}
}
}
update_user_meta( $essay->post_author, '_sfwd-quizzes', $users_quiz_data );
/**
* Fires after the essay response data is updated.
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param WP_Post $essay WP_Post object for essay.
* @param array $submitted_essay An array of submitted essay data.
*/
do_action( 'learndash_essay_response_data_updated', $quiz_id, $question_id, $essay, $submitted_essay );
}
/**
* Updates the user's quiz data.
*
* Finds this particular quiz attempt in the user's meta and updates its data.
*
* @since 2.2.0
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param array $updated_scoring An array of updated quiz scoring data.
* @param WP_Post $essay The `WP_Post` essay object.
*/
function learndash_update_quiz_data( $quiz_id, $question_id, $updated_scoring, $essay ) {
$affected_quiz_keys = array();
$users_quiz_data = get_user_meta( $essay->post_author, '_sfwd-quizzes', true );
if ( ( $essay ) && ( is_a( $essay, 'WP_Post' ) ) ) {
$essay_quiz_time = get_post_meta( $essay->ID, 'quiz_time', true );
} else {
$essay_quiz_time = null;
}
// We need to find the user meta quiz to matches the essay being scored.
foreach ( $users_quiz_data as $quiz_key => $quiz_data ) {
// We check for a match on the quiz time from the essay postmeta first.
// If the essay_quiz_time is not empty and does NOT match then continue.
if ( ( absint( $essay_quiz_time ) ) && ( isset( $quiz_data['time'] ) ) && ( absint( $essay_quiz_time ) !== absint( $quiz_data['time'] ) ) ) {
continue;
}
if ( ( $quiz_id != $quiz_data['pro_quizid'] ) || ( ! isset( $quiz_data['has_graded'] ) ) || ( false == $quiz_data['has_graded'] ) ) {
continue;
}
if ( ( ! isset( $quiz_data['graded'][ $question_id ]['post_id'] ) ) || ( $quiz_data['graded'][ $question_id ]['post_id'] != $essay->ID ) ) {
continue;
}
$affected_quiz_keys[] = $quiz_key;
// update total score.
$users_quiz_data[ $quiz_key ]['score'] = $users_quiz_data[ $quiz_key ]['score'] + $updated_scoring['score_difference'];
// update total points.
$users_quiz_data[ $quiz_key ]['points'] = $users_quiz_data[ $quiz_key ]['points'] + $updated_scoring['points_awarded_difference'];
$total_points_setting = isset( $users_quiz_data[ $quiz_key ]['total_points'] )
? Cast::to_float( $users_quiz_data[ $quiz_key ]['total_points'] )
: 0.00;
// update total score percentage.
$updated_percentage = $total_points_setting > 0
? ( $users_quiz_data[ $quiz_key ]['points'] / $total_points_setting ) * 100
: 100;
$users_quiz_data[ $quiz_key ]['percentage'] = round( $updated_percentage, 2 );
// update passing score.
$quizmeta = get_post_meta( $quiz_data['quiz'], '_sfwd-quiz', true );
$passingpercentage = intVal( $quizmeta['sfwd-quiz_passingpercentage'] );
$users_quiz_data[ $quiz_key ]['pass'] = ( $users_quiz_data[ $quiz_key ]['percentage'] >= $passingpercentage ) ? 1 : 0;
learndash_update_quiz_statistics( $quiz_id, $question_id, $updated_scoring, $essay, $users_quiz_data[ $quiz_key ] );
learndash_update_quiz_activity( $essay->post_author, $users_quiz_data[ $quiz_key ] );
}
update_user_meta( $essay->post_author, '_sfwd-quizzes', $users_quiz_data );
if ( ! empty( $affected_quiz_keys ) ) {
foreach ( $affected_quiz_keys as $quiz_key ) {
if ( isset( $users_quiz_data[ $quiz_key ] ) ) {
$quiz_id = isset( $users_quiz_data[ $quiz_key ]['quiz'] )
? Cast::to_int( $users_quiz_data[ $quiz_key ]['quiz'] )
: 0;
$course_id = isset( $users_quiz_data[ $quiz_key ]['course'] )
? Cast::to_int( $users_quiz_data[ $quiz_key ]['course'] )
: Cast::to_int( learndash_get_course_id( $essay->ID ) );
// If the quiz ID is invalid, we can't process the step completion.
// This is a safety check to prevent warnings, but it should never happen.
if ( $quiz_id <= 0 ) {
continue;
}
$send_quiz_completed = true;
if ( ( isset( $users_quiz_data[ $quiz_key ]['has_graded'] ) ) && ( true === $users_quiz_data[ $quiz_key ]['has_graded'] ) ) {
if ( ( isset( $users_quiz_data[ $quiz_key ]['graded'] ) ) && ( ! empty( $users_quiz_data[ $quiz_key ]['graded'] ) ) ) {
foreach ( $users_quiz_data[ $quiz_key ]['graded'] as $grade_item ) {
if ( ( isset( $grade_item['status'] ) ) && ( 'graded' !== $grade_item['status'] ) ) {
$send_quiz_completed = false;
}
}
}
}
// Check if the quiz has not been completed. We may have all the questions graded, but the user doesn't have enough points to pass.
if (
$send_quiz_completed
&& learndash_is_quiz_notcomplete(
Cast::to_int( $essay->post_author ),
[ $quiz_id => 1 ],
false,
$course_id
)
) {
$send_quiz_completed = false;
}
if ( $send_quiz_completed ) {
// Process the course step completion.
learndash_process_mark_complete( $essay->post_author, $quiz_id, false, $course_id );
/** This action is documented in includes/ld-users.php */
do_action( 'learndash_quiz_completed', $users_quiz_data[ $quiz_key ], get_user_by( 'ID', $essay->post_author ) );
}
}
}
}
/**
* Fires after the essay quiz data is updated.
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param array $updated_scoring An array of updated essay scoring data.
* @param WP_Post $essay WP_Post object for essay.
*/
do_action( 'learndash_essay_quiz_data_updated', $quiz_id, $question_id, $updated_scoring, $essay );
}
/**
* Updates the quiz activity for a user.
*
* @since 2.3.0
*
* @param int $user_id User ID.
* @param array $quiz_data An array of quiz activity data to be updated.
*/
function learndash_update_quiz_activity( $user_id = 0, $quiz_data = array() ) {
if ( ( ! empty( $user_id ) ) && ( ! empty( $quiz_data ) ) ) {
$quiz_data_meta = $quiz_data;
// Remove many fields that we either don't need or are duplicate of the main table columns.
unset( $quiz_data_meta['quiz'] );
unset( $quiz_data_meta['pro_quizid'] );
unset( $quiz_data_meta['time'] );
unset( $quiz_data_meta['completed'] );
unset( $quiz_data_meta['started'] );
if ( '-' == $quiz_data_meta['rank'] ) {
unset( $quiz_data_meta['rank'] );
}
if ( true == $quiz_data['pass'] ) {
$quiz_data_pass = true;
} else {
$quiz_data_pass = false;
}
learndash_update_user_activity(
array(
'course_id' => ( isset( $quiz_data['course'] ) ) ? intval( $quiz_data['course'] ) : 0,
'post_id' => $quiz_data['quiz'],
'user_id' => $user_id,
'activity_type' => 'quiz',
'activity_status' => $quiz_data_pass,
'activity_started' => $quiz_data['started'],
'activity_completed' => $quiz_data['completed'],
'activity_meta' => $quiz_data_meta,
)
);
}
}
/**
* Updates the quiz statistics for the given quiz attempt.
*
* Updates the score when the essay grading is adjusted, I ran this through manual SQL queries
* because WpProQuiz doesn't offer an elegant way to grab a particular question and update it.
*
* Important: It is currently used for updating the essay question stats only, even though it has a generic name.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @since 2.2.0
*
* @param int $quiz_id Quiz ID.
* @param int $question_id Question ID.
* @param array $updated_quiz_data Updated quiz statistics data.
* @param WP_Post $essay The `WP_Post` essay object.
* @param array $users_quiz_data User quiz data.
*/
function learndash_update_quiz_statistics( $quiz_id, $question_id, $updated_quiz_data, $essay, $users_quiz_data ) {
global $wpdb;
if ( ( isset( $users_quiz_data['statistic_ref_id'] ) ) && ( ! empty( $users_quiz_data['statistic_ref_id'] ) ) ) {
$ref_id = absint( $users_quiz_data['statistic_ref_id'] );
} else {
$ref_id = $wpdb->get_var(
$wpdb->prepare(
'
SELECT statistic_ref_id
FROM ' . LDLMS_DB::get_table_name( 'quiz_statistic_ref' ) . ' WHERE quiz_id = %d AND user_id = %d
',
$quiz_id,
$essay->post_author
)
);
$ref_id = absint( $ref_id );
}
$row = $wpdb->get_results(
$wpdb->prepare(
'
SELECT *
FROM ' . LDLMS_DB::get_table_name( 'quiz_statistic' ) . ' WHERE statistic_ref_id = %d AND question_id = %d
',
$ref_id,
$question_id
)
);
if ( empty( $row ) ) {
return;
}
if ( $updated_quiz_data['updated_question_score'] > 0 ) {
$correct_count = 1;
$incorrect_count = 0;
} else {
$correct_count = 0;
$incorrect_count = 1;
}
$update = $wpdb->update(
LDLMS_DB::get_table_name( 'quiz_statistic' ),
array(
'correct_count' => $correct_count,
'incorrect_count' => $incorrect_count,
'points' => learndash_format_course_points( $updated_quiz_data['updated_question_score'] ),
),
array(
'statistic_ref_id' => $ref_id,
'question_id' => $question_id,
),
array( '%d', '%d', '%.2f' ),
array( '%d', '%d' )
);
/**
* Fires after the essay question stats are updated.
*/
do_action( 'learndash_essay_question_stats_updated' );
}
/**
* Handles the AJAX file upload for an essay question.
*
* Fires on `learndash_upload_essay` AJAX action.
*
* @since 2.2.0
*
* Runs checks for needing information, or will die and send an error back to browser
*/
function learndash_upload_essay() {
if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['question_id'] ) || ! isset( $_FILES['essayUpload'] ) ) {
wp_send_json_error();
die();
}
$nonce = $_POST['nonce'];
$question_id = intval( $_POST['question_id'] );
if ( empty( $question_id ) ) {
wp_send_json_error();
die();
}
/**
* Changes in v2.5.4 to include the question_id as part of the nonce
*/
if ( ! wp_verify_nonce( $nonce, 'learndash-upload-essay-' . $question_id ) ) {
wp_send_json_error();
die( 'Security check' );
} else {
if ( ! is_user_logged_in() ) {
/**
* Filters whether to allow essay upload or not if the user is not logged in.
*
* @param boolean $allow_upload Whether to allow upload.
* @param int $question_id ID of the essay question.
*/
if ( ! apply_filters( 'learndash_essay_upload_user_check', false, $question_id ) ) {
wp_send_json_error();
die();
}
}
$file_desc = learndash_essay_fileupload_process( $_FILES['essayUpload'], $question_id );
if ( ! empty( $file_desc ) ) {
wp_send_json_success( $file_desc );
} else {
wp_send_json_error(
array(
'message' => esc_html__( 'Unknown error.', 'learndash' ),
)
);
}
die();
}
}
add_action( 'wp_ajax_learndash_upload_essay', 'learndash_upload_essay' );
add_action( 'wp_ajax_nopriv_learndash_upload_essay', 'learndash_upload_essay' );
/**
* Handles the file uploads for the essays.
*
* @since 2.2.0
*
* @param array $uploadfiles An array of uploaded files data.
* @param int $question_id Question ID.
*
* @return array An array of file data like file name and link.
*/
function learndash_essay_fileupload_process( $uploadfiles, $question_id ) {
if ( is_array( $uploadfiles ) ) {
// look only for uploaded files.
if ( 0 == $uploadfiles['error'] ) {
$file_tmp = $uploadfiles['tmp_name'];
// clean filename.
$filename = learndash_clean_filename( $uploadfiles['name'] );
// extract extension.
if ( ! function_exists( 'wp_get_current_user' ) ) {
include ABSPATH . 'wp-includes/pluggable.php';
}
// current user.
$user = get_current_user_id();
$limit_file_exts = learndash_get_allowed_upload_mime_extensions_for_post( $question_id );
// get file info.
// @fixme: wp checks the file extension....
$filetype = wp_check_filetype( basename( $filename ), $limit_file_exts );
if ( ( empty( $filetype ) ) || ( empty( $filetype['ext'] ) ) || ( empty( $filetype['type'] ) ) || ( ! $limit_file_exts[ strtolower( $filetype['ext'] ) ] ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Invalid essay uploaded file type.', 'learndash' ),
)
);
die();
}
$filetype['ext'] = strtolower( $filetype['ext'] );
$file_title = pathinfo( $filename, PATHINFO_FILENAME );
$file_time = microtime( true ) * 100;
$filename = sprintf( 'question_%d_%d_%s_%s.%s', $question_id, $file_time, $file_title, uniqid(), $filetype['ext'] );
/** This filter is documented in includes/import/class-ld-import-quiz-statistics.php */
$filename = apply_filters( 'learndash_essay_upload_filename', $filename, $question_id, $file_title, $filetype['ext'] );
$upload_dir = wp_upload_dir();
$upload_dir_base = str_replace( '\\', DIRECTORY_SEPARATOR, $upload_dir['basedir'] );
$upload_url_base = $upload_dir['baseurl'];
/** This filter is documented in includes/import/class-ld-import-quiz-statistics.php */
$upload_dir_path = $upload_dir_base . apply_filters( 'learndash_essay_upload_dirbase', DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'essays', $filename, $upload_dir );
/** This filter is documented in includes/import/class-ld-import-quiz-statistics.php */
$upload_url_path = $upload_url_base . apply_filters( 'learndash_essay_upload_urlbase', DIRECTORY_SEPARATOR . 'learndash' . DIRECTORY_SEPARATOR . 'essays' . DIRECTORY_SEPARATOR, $filename, $upload_dir );
if ( ! file_exists( $upload_dir_path ) ) {
if ( is_writable( dirname( $upload_dir_path ) ) ) {
wp_mkdir_p( $upload_dir_path );
} else {
wp_send_json_error(
array(
'message' => esc_html__( 'Unable to write to UPLOADS directory. Is this directory writable by the server?', 'learndash' ),
)
);
die();
}
}
// Add an index.php file to prevent directory browsing.
$_index = trailingslashit( $upload_dir_path ) . 'index.php';
if ( ! file_exists( $_index ) ) {
file_put_contents( $_index, '//LearnDash is THE Best LMS' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents -- It's okay here.
}
$file_title = pathinfo( basename( $filename ), PATHINFO_FILENAME );
$file_ext = pathinfo( basename( $filename ), PATHINFO_EXTENSION );
/**
* Check if the filename already exist in the directory and rename the
* file if necessary
*/
$i = 0;
while ( file_exists( $upload_dir_path . DIRECTORY_SEPARATOR . $filename ) ) {
++$i;
$filename = $file_title . '_' . $i . '.' . $file_ext;
}
$file_dest = $upload_dir_path . DIRECTORY_SEPARATOR . $filename;
$destination = $upload_url_path . $filename;
/**
* Check write permissions
*/
if ( ! is_writeable( $upload_dir_path ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Unable to write to directory. Is this directory writable by the server?', 'learndash' ),
)
);
die();
}
/**
* 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.
wp_send_json_error(
array(
'message' => esc_html__( 'The uploaded file could not be move to the destination directory.', 'learndash' ),
)
);
die();
}
$file_desc = array();
$file_desc['filename'] = $filename;
$file_desc['filelink'] = $destination;
$file_desc['message'] = esc_html__( 'Essay upload success.', 'learndash' );
return $file_desc;
}
}
return array();
}
/**
* Deletes the uploaded file when essay post is deleted.
*
* Fires on `before_delete_post` hook.
*
* @since 2.5.0
*
* @param int $post_id Post ID.
*/
function learndash_before_delete_essay( $post_id ) {
if ( ( ! empty( $post_id ) ) && ( 'sfwd-essays' == get_post_type( $post_id ) ) ) {
$file_path = get_post_meta( $post_id, 'upload', true );
if ( ! empty( $file_path ) ) {
$url_link_arr = wp_upload_dir();
$base_dir = trailingslashit( str_replace( '\\', DIRECTORY_SEPARATOR, $url_link_arr['basedir'] ) );
$file_name = basename( Cast::to_string( $file_path ) );
// Since LD version 4.10.3 we've changed the file path and added the learndash_version meta.
$file_path = get_post_meta( $post_id, 'learndash_version', true )
? $base_dir . 'learndash' . DIRECTORY_SEPARATOR . 'essays' . DIRECTORY_SEPARATOR . $file_name
: $base_dir . 'essays' . DIRECTORY_SEPARATOR . $file_name;
if ( file_exists( $file_path ) ) {
unlink( $file_path );
}
}
}
}
add_action( 'before_delete_post', 'learndash_before_delete_essay' );
/**
* Updates the essays post meta with a reference to the quiz attempt user meta.
*
* Fires on `learndash_quiz_submitted` hook.
*
* @since 3.1.0
*
* @param array $quizdata Optional. An array of quiz attempt data. Default empty array.
* @param WP_User $user The `WP_User` instance.
*/
function learndash_quiz_submitted_update_essay( $quizdata, $user ) {
if ( is_array( $quizdata ) ) {
if ( ( isset( $quizdata['time'] ) ) && ( ! empty( $quizdata['time'] ) ) ) {
if ( ( isset( $quizdata['has_graded'] ) ) && ( true === $quizdata['has_graded'] ) ) {
if ( ( isset( $quizdata['graded'] ) ) && ( ! empty( $quizdata['graded'] ) ) ) {
foreach ( $quizdata['graded'] as $question_id => $graded_data ) {
if ( isset( $graded_data['post_id'] ) ) {
$essay_post_id = absint( $graded_data['post_id'] );
if ( ! empty( $essay_post_id ) ) {
// Update the Essay post_status.
if ( isset( $graded_data['status'] ) ) {
$essay_post = array(
'ID' => $essay_post_id,
'post_status' => esc_attr( $graded_data['status'] ),
);
wp_update_post( $essay_post );
}
$quiz_time = get_post_meta( $essay_post_id, 'quiz_time', true );
if ( ! $quiz_time ) {
update_post_meta( $essay_post_id, 'quiz_time', $quizdata['time'] );
}
}
}
}
}
}
}
}
}
add_action( 'learndash_quiz_submitted', 'learndash_quiz_submitted_update_essay', 1, 2 );
/**
* Return the Usermeta Quiz array for the Essay Post ID.
*
* @since 3.2.3
* @param int $essay_post_id Essay Post ID.
* @param int $user_id User ID.
*/
function learndash_get_user_quiz_entry_for_essay( $essay_post_id = 0, $user_id = 0 ) {
$essay_post_id = absint( $essay_post_id );
if ( ! empty( $essay_post_id ) ) {
$essay_post = get_post( $essay_post_id );
if ( ( $essay_post ) && ( is_a( $essay_post, 'WP_Post' ) ) && ( learndash_get_post_type_slug( 'essay' ) === $essay_post->post_type ) ) {
if ( empty( $user_id ) ) {
$user_id = absint( $essay_post->post_author );
}
$quiz_pro_id = get_post_meta( $essay_post->ID, 'quiz_pro_id', true );
$quiz_time = get_post_meta( $essay_post->ID, 'quiz_time', true );
if ( ( ! empty( $user_id ) ) && ( ! empty( $quiz_pro_id ) ) && ( ! empty( $quiz_time ) ) ) {
$user_quizzes = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ! empty( $user_quizzes ) ) {
foreach ( $user_quizzes as $q_idx => $user_quiz ) {
if ( ( isset( $user_quiz['pro_quizid'] ) ) && ( absint( $quiz_pro_id ) === absint( $user_quiz['pro_quizid'] ) ) ) {
if ( ( isset( $user_quiz['time'] ) ) && ( absint( $quiz_time ) === absint( $user_quiz['time'] ) ) ) {
if ( ( isset( $user_quiz['graded'] ) ) && ( ! empty( $user_quiz['graded'] ) ) ) {
foreach ( $user_quiz['graded'] as $key => $graded_question ) {
if ( absint( $essay_post_id ) === absint( $graded_question['post_id'] ) ) {
$user_quiz['usermeta_idx'] = absint( $q_idx );
return $user_quiz;
}
}
}
}
}
}
}
}
}
}
}
/**
* Returns the URL to download the quiz essay file, if any.
*
* @since 4.10.3
*
* @param int $post_id Essay ID.
*
* @return string Empty string if no file, otherwise the URL to download the file.
*/
function learndash_quiz_essay_get_download_url( int $post_id ): string {
$download_url = '';
$file_name = Cast::to_string(
get_post_meta( $post_id, 'upload', 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_essays'
: 'uploads_essays';
try {
$download_url = File_Download_Handler::get_download_url( $file_path_id, basename( $file_name ) );
} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Ignore.
}
}
/**
* Filters the returned essay download URL.
*
* @since 4.19.0
*
* @param string $download_url The URL to download the essay file.
* @param int $post_id Essay Post ID.
* @param string $file_name Essay file name.
*
* @return string The URL to download the essay file.
*/
return apply_filters(
'learndash_quiz_essay_get_download_url',
$download_url,
$post_id,
$file_name
);
}