diff --git a/assets/src/js/front/tutor-front.js b/assets/src/js/front/tutor-front.js index a9fe934d87..6b52057367 100644 --- a/assets/src/js/front/tutor-front.js +++ b/assets/src/js/front/tutor-front.js @@ -274,6 +274,11 @@ jQuery(document).ready(function($) { return; } + // If user is not enrolled like lesson preview. + if (!video_data.is_enrolled) { + return; + } + if (this.isRequiredPercentage()) { this.enable_complete_lesson_btn(instance); } diff --git a/classes/Addons.php b/classes/Addons.php index bde10328ae..5b1de32e35 100644 --- a/classes/Addons.php +++ b/classes/Addons.php @@ -10,9 +10,8 @@ namespace TUTOR; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; + /** * Addons Class * @@ -25,6 +24,7 @@ class Addons { * Constructor * * @since 1.0.0 + * * @return void */ public function __construct() { @@ -52,6 +52,7 @@ public static function get_addons_config() { * * @param string $basename basename of addon. * @param bool $status status 0,1. + * * @return void */ public static function update_addon_status( $basename, $status ) { @@ -66,6 +67,7 @@ public static function update_addon_status( $basename, $status ) { * Get all addons data. * * @since 1.0.0 + * * @return void */ public function get_all_addons() { @@ -179,8 +181,8 @@ public function addon_enable_disable() { tutor_utils()->checking_nonce(); - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) ); + if ( ! User::is_admin() ) { + wp_send_json_error( tutor_utils()->error_message() ); } $form_data = json_decode( Input::post( 'addonFieldNames' ) ); @@ -227,6 +229,7 @@ public function addon_enable_disable() { * Get tutor addons list * * @since 1.0.0 + * * @return array */ public function addons_lists_to_show() { diff --git a/classes/Admin.php b/classes/Admin.php index 09bb21814a..7d1d609c60 100644 --- a/classes/Admin.php +++ b/classes/Admin.php @@ -10,16 +10,14 @@ namespace TUTOR; +defined( 'ABSPATH' ) || exit; + use Tutor\Ecommerce\OrderController; use Tutor\Helpers\HttpHelper; use TUTOR\Input; use Tutor\Models\CourseModel; use Tutor\Traits\JsonResponse; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - /** * Admin Class * diff --git a/classes/Ajax.php b/classes/Ajax.php index 993986268e..f26b171a5d 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -10,9 +10,7 @@ namespace TUTOR; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; use Tutor\GDPR\Controllers\LegalConsent; use Tutor\Helpers\HttpHelper; @@ -28,6 +26,7 @@ class Ajax { use JsonResponse; const LOGIN_ERRORS_TRANSIENT_KEY = 'tutor_login_errors'; + /** * Constructor * @@ -41,7 +40,6 @@ class Ajax { public function __construct( $allow_hooks = true ) { if ( $allow_hooks ) { add_action( 'wp_ajax_sync_video_playback', array( $this, 'sync_video_playback' ) ); - add_action( 'wp_ajax_nopriv_sync_video_playback', array( $this, 'sync_video_playback_noprev' ) ); add_action( 'wp_ajax_tutor_place_rating', array( $this, 'tutor_place_rating' ) ); add_action( 'wp_ajax_delete_tutor_review', array( $this, 'delete_tutor_review' ) ); @@ -60,8 +58,8 @@ public function __construct( $allow_hooks = true ) { * * @since v.1.7.9 */ - add_action( 'wp_ajax_tutor_announcement_create', array( $this, 'create_or_update_annoucement' ) ); - add_action( 'wp_ajax_tutor_announcement_delete', array( $this, 'delete_annoucement' ) ); + add_action( 'wp_ajax_tutor_announcement_create', array( $this, 'create_or_update_announcement' ) ); + add_action( 'wp_ajax_tutor_announcement_delete', array( $this, 'delete_announcement' ) ); add_action( 'wp_ajax_tutor_youtube_video_duration', array( $this, 'ajax_youtube_video_duration' ) ); } @@ -73,6 +71,7 @@ public function __construct( $allow_hooks = true ) { * Update video information and data when necessary * * @since 1.0.0 + * * @return void */ public function sync_video_playback() { @@ -116,15 +115,6 @@ public function sync_video_playback() { exit(); } - /** - * Video playback callback for noprev - * - * @since 1.0.0 - * @return void - */ - public function sync_video_playback_noprev() { - } - /** * Place rating * @@ -396,9 +386,7 @@ public function add_or_delete_wishlist( $user_id, $course_id ) { * Process tutor login * * @since 1.6.3 - * - * @since 2.1.3 Ajax removed, validation errors - * stores in session. + * @since 2.1.3 Ajax removed, validation errors stores in session. * * @return void */ @@ -411,9 +399,9 @@ public function process_tutor_login() { * * @since 2.1.4 */ - if ( ! wp_verify_nonce( $_POST[ tutor()->nonce ], tutor()->nonce_action ) ) { //phpcs:ignore + if ( ! tutor_utils()->is_nonce_verified( 'post' ) ) { $validation_error->add( 401, __( 'Nonce verification failed', 'tutor' ) ); - \set_transient( self::LOGIN_ERRORS_TRANSIENT_KEY, $validation_error->get_error_messages() ); + \set_transient( self::LOGIN_ERRORS_TRANSIENT_KEY, $validation_error->get_error_messages(), MINUTE_IN_SECONDS ); return; } @@ -425,10 +413,12 @@ public function process_tutor_login() { * * @see https://developer.wordpress.org/reference/functions/wp_signon/ */ - $username = tutor_utils()->array_get( 'log', $_POST ); //phpcs:ignore - $password = tutor_utils()->array_get( 'pwd', $_POST ); //phpcs:ignore + //phpcs:disable WordPress.Security.NonceVerification.Missing + $username = tutor_utils()->array_get( 'log', $_POST ); //phpcs:ignore + $password = tutor_utils()->array_get( 'pwd', $_POST ); //phpcs:ignore $redirect_to = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : ''; $remember = isset( $_POST['rememberme'] ); + //phpcs:enable WordPress.Security.NonceVerification.Missing try { $creds = array( @@ -497,7 +487,7 @@ public function process_tutor_login() { $validation_error->add( 400, $e->getMessage() ); } finally { // Store errors in transient data. - \set_transient( self::LOGIN_ERRORS_TRANSIENT_KEY, $validation_error->get_error_messages() ); + \set_transient( self::LOGIN_ERRORS_TRANSIENT_KEY, $validation_error->get_error_messages(), MINUTE_IN_SECONDS ); } } @@ -505,9 +495,10 @@ public function process_tutor_login() { * Create/Update announcement * * @since 1.7.9 + * * @return void */ - public function create_or_update_annoucement() { + public function create_or_update_announcement() { tutor_utils()->checking_nonce(); $error = array(); @@ -515,7 +506,7 @@ public function create_or_update_annoucement() { $announcement_title = Input::post( 'tutor_announcement_title' ); $announcement_summary = Input::post( 'tutor_announcement_summary', '', Input::TYPE_TEXTAREA ); - // Check if user can manage this announcment. + // Check if user can manage this announcement. if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) { wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) ); } @@ -588,9 +579,10 @@ public function create_or_update_annoucement() { * Delete announcement * * @since 1.7.9 + * * @return void */ - public function delete_annoucement() { + public function delete_announcement() { tutor_utils()->checking_nonce(); $announcement_id = Input::post( 'announcement_id' ); diff --git a/classes/Announcements.php b/classes/Announcements.php index a3b9c84f9e..b51d9156a5 100644 --- a/classes/Announcements.php +++ b/classes/Announcements.php @@ -10,12 +10,11 @@ namespace TUTOR; +defined( 'ABSPATH' ) || exit; + use Tutor\Helpers\QueryHelper; use Tutor\Helpers\UrlHelper; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} /** * Announcements class * @@ -41,6 +40,7 @@ class Announcements { * Constructor * * @since 1.0.0 + * * @return void */ public function __construct() { @@ -74,6 +74,7 @@ public function __get( $name ) { * Prepare bulk actions that will show on dropdown options * * @since 2.0.0 + * * @return array */ public function prepare_bulk_actions(): array { @@ -88,6 +89,7 @@ public function prepare_bulk_actions(): array { * Handle bulk action for enrollment cancel | delete * * @since 2.0.0 + * * @return string JSON response. */ public function announcement_bulk_action() { @@ -118,7 +120,7 @@ function ( $announcement_id ) { * @since 2.0.0 * * @param string $action hold action. - * @param string $bulk_ids comma seperated ids. + * @param string $bulk_ids comma separated ids. * * @return bool */ diff --git a/classes/Course.php b/classes/Course.php index 7b6e4ec217..35d6edacaa 100644 --- a/classes/Course.php +++ b/classes/Course.php @@ -10,9 +10,7 @@ namespace TUTOR; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; use Tutor\Components\Button; use Tutor\Components\Constants\Size; @@ -942,13 +940,12 @@ public function ajax_create_new_draft_course() { * * @since 3.0.0 * - * @since 3.2.0 - * - * Refactor the arguments & response as per new design + * @since 3.2.0 Refactor the arguments & response as per new design. * * @return void */ public function ajax_course_list() { + tutor_utils()->check_nonce(); $this->check_access(); $limit = Input::post( 'limit', 10, Input::TYPE_INT ); @@ -970,12 +967,7 @@ public function ajax_course_list() { $exclude = Input::post( 'exclude', array(), Input::TYPE_ARRAY ); if ( count( $exclude ) ) { - $exclude = array_filter( - $exclude, - function ( $id ) { - return is_numeric( $id ); - } - ); + $exclude = array_filter( $exclude, fn( $id ) => is_numeric( $id ) && $id > 0 ); $args['post__not_in'] = $exclude; } @@ -1117,6 +1109,10 @@ public function ajax_update_course() { ); $course_id = (int) $params['course_id']; + if ( ! $course_id ) { + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) ); + } + $this->check_access( $course_id ); $errors = array(); @@ -1207,9 +1203,13 @@ public function ajax_unlink_page_builder() { tutor_utils()->check_nonce(); $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); - $builder = Input::post( 'builder' ); + if ( ! $course_id ) { + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) ); + } + $this->check_access( $course_id ); + $builder = Input::post( 'builder' ); if ( 'elementor' === $builder ) { delete_post_meta( $course_id, '_elementor_edit_mode' ); } elseif ( 'droip' === $builder ) { @@ -1284,18 +1284,21 @@ public function get_course_contents( $course_id ) { * Get course contents * * @since 3.0.0 + * + * @return void */ public function ajax_course_contents() { tutor_utils()->check_nonce(); + $errors = array(); $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); - $this->check_access( $course_id ); - - if ( tutor()->course_post_type !== get_post_type( $course_id ) ) { + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) { $errors['course_id'] = __( 'Invalid course id', 'tutor' ); } + $this->check_access( $course_id ); + if ( ! empty( $errors ) ) { $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY ); } @@ -1321,12 +1324,12 @@ public function ajax_course_details() { $errors = array(); $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); - $this->check_access( $course_id ); - - if ( tutor()->course_post_type !== get_post_type( $course_id ) ) { + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) { $errors['course_id'] = __( 'Invalid course id', 'tutor' ); } + $this->check_access( $course_id ); + if ( ! empty( $errors ) ) { $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY ); } @@ -1339,7 +1342,7 @@ public function ajax_course_details() { $sale_price = 0; $product_id = tutor_utils()->get_course_product_id( $course_id ); - if ( 'wc' === $monetize_by ) { + if ( WooCommerce::MONETIZE_BY === $monetize_by ) { $product = wc_get_product( $product_id ); if ( $product ) { $product_name = $product->get_name(); @@ -1348,7 +1351,7 @@ public function ajax_course_details() { } } - if ( 'tutor' === $monetize_by ) { + if ( Ecommerce::MONETIZE_BY === $monetize_by ) { $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true ); $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true ); } @@ -1802,7 +1805,9 @@ public function restrict_new_student_entry( $content ) { * Restrict media * * @since 1.0.0 + * * @param string $where where clause. + * * @return string */ public function restrict_media( $where ) { @@ -2349,6 +2354,7 @@ public function clear_review_popup_data() { * Delete course delete from frontend dashboard * * @since 2.0.0 + * * @return void */ public function tutor_delete_dashboard_course() { @@ -2369,7 +2375,7 @@ public function tutor_delete_dashboard_course() { } // Check if user is only an instructor. - if ( ! current_user_can( 'administrator' ) ) { + if ( ! User::is_admin() ) { // Check if instructor can trash course. $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' ); @@ -2381,7 +2387,7 @@ public function tutor_delete_dashboard_course() { $trash_course = wp_update_post( array( 'ID' => $course_id, - 'post_status' => 'trash', + 'post_status' => CourseModel::STATUS_TRASH, ) ); @@ -2563,6 +2569,7 @@ public function attach_product_with_course( $post_ID, $post_data ) { * @since 1.4.1 * * @param array $args arguments. + * * @return array */ public function add_course_level_to_settings( $args ) { @@ -2604,6 +2611,7 @@ public function tutor_lesson_load_before() { * Add Course level to course settings * * @since 1.4.8 + * * @return void */ public function course_elements_enable_disable() { @@ -2638,6 +2646,7 @@ public function enable_disable_course_progress_bar( $html ) { * @since 1.4.8 * * @param string $html HTML string. + * * @return string */ public function enable_disable_material_includes( $html ) { @@ -2654,6 +2663,7 @@ public function enable_disable_material_includes( $html ) { * @since 1.4.8 * * @param string $html HTML string. + * * @return string */ public function enable_disable_course_content( $html ) { @@ -2670,6 +2680,7 @@ public function enable_disable_course_content( $html ) { * @since 1.4.8 * * @param string $html HTML string. + * * @return string */ public function enable_disable_course_benefits( $html ) { @@ -2686,6 +2697,7 @@ public function enable_disable_course_benefits( $html ) { * @since 1.4.8 * * @param string $html HTML string. + * * @return string */ public function enable_disable_course_requirements( $html ) { @@ -2702,6 +2714,7 @@ public function enable_disable_course_requirements( $html ) { * @since 1.4.8 * * @param string $html HTML string. + * * @return string */ public function enable_disable_course_target_audience( $html ) { @@ -2765,6 +2778,7 @@ function ( $item ) use ( $is_enrolled ) { * Filter product in shop page * * @since 1.4.9 + * * @return void|null */ public function filter_product_in_shop_page() { @@ -2782,6 +2796,7 @@ public function filter_product_in_shop_page() { * Tutor product meta query * * @since 1.4.9 + * * @return array */ public function tutor_product_meta_query() { @@ -2798,6 +2813,7 @@ public function tutor_product_meta_query() { * @since 1.4.9 * * @param \WP_Query $wp_query WP Query instance. + * * @return \WP_Query */ public function filter_woocommerce_product_query( $wp_query ) { @@ -2841,6 +2857,7 @@ public function get_connected_wc_product_ids() { * @since 1.4.9 * * @param \WP_Query $query WP Query instance. + * * @return \WP_Query */ public function filter_edd_downloads_query( $query ) { @@ -2854,6 +2871,7 @@ public function filter_edd_downloads_query( $query ) { * @since 1.4.9 * * @param \WP_Query $wp_query WP Query instance. + * * @return \WP_Query */ public function filter_archive_meta_query( $wp_query ) { @@ -2869,6 +2887,7 @@ public function filter_archive_meta_query( $wp_query ) { * @since 1.5.8 * * @param string $html HTML string. + * * @return string */ public function remove_price_if_enrolled( $html ) { @@ -2989,6 +3008,7 @@ private static function get_course_completion_requirement_message( $quiz_count, * @since 1.5.8 * * @param string $html HTML string. + * * @return string */ public function tutor_lms_hide_course_complete_btn( $html ) { @@ -3012,6 +3032,7 @@ public function tutor_lms_hide_course_complete_btn( $html ) { * @since 1.5.8 * * @param string $html HTML string. + * * @return string */ public function get_generate_greadbook( $html ) { @@ -3025,6 +3046,7 @@ public function get_generate_greadbook( $html ) { * Add social share content in header * * @since 1.6.3 + * * @return void */ public function social_share_content() { @@ -3050,6 +3072,7 @@ public function social_share_content() { * @since 1.8.2 * * @param integer $post_id post ID. + * * @return void */ public function delete_associated_enrollment( $post_id ) { @@ -3083,6 +3106,7 @@ public function delete_associated_enrollment( $post_id ) { * Reset course progress. * * @since 1.5.8 + * * @return void */ public function tutor_reset_course_progress() { @@ -3118,7 +3142,7 @@ public function tutor_reset_course_progress() { * * @param integer $course_id course ID. * @param integer $user_id user ID. - + * * @return void */ public function enroll_after_login_if_attempt( int $course_id, int $user_id ) { @@ -3138,6 +3162,7 @@ public function enroll_after_login_if_attempt( int $course_id, int $user_id ) { * Handle course enrollment * * @since 2.1.0 + * * @return void */ public function course_enrollment() { @@ -3146,44 +3171,44 @@ public function course_enrollment() { $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); $user_id = get_current_user_id(); - if ( $course_id ) { - $password_protected = post_password_required( $course_id ); - if ( $password_protected ) { - wp_send_json_error( __( 'This course is password protected', 'tutor' ) ); - } + if ( ! $course_id || ! $user_id ) { + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) ); + } - $course = get_post( $course_id ); + $password_protected = post_password_required( $course_id ); + if ( $password_protected ) { + wp_send_json_error( __( 'This course is password protected', 'tutor' ) ); + } - if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) { - wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) ); - } + $course = get_post( $course_id ); - /** - * This check was added to address a security issue where users could - * enroll in a course via an AJAX call without purchasing it. - * - * To prevent this, we now verify whether the course is paid. - * Additionally, we check if the user is already enrolled, since - * Tutor's default behavior enrolls users automatically upon purchase. - * - * @since 3.9.4 - */ - if ( tutor_utils()->is_course_purchasable( $course_id ) ) { - $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id ); - - if ( ! $is_enrolled ) { - wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) ); - } - } + if ( CourseModel::STATUS_PRIVATE === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) { + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) ); + } - $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id ); - if ( $enroll ) { - wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) ); - } else { - wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) ); + /** + * This check was added to address a security issue where users could + * enroll in a course via an AJAX call without purchasing it. + * + * To prevent this, we now verify whether the course is paid. + * Additionally, we check if the user is already enrolled, since + * Tutor's default behavior enrolls users automatically upon purchase. + * + * @since 3.9.4 + */ + if ( tutor_utils()->is_course_purchasable( $course_id ) ) { + $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id ); + + if ( ! $is_enrolled ) { + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) ); } + } + + $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id ); + if ( $enroll ) { + wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) ); } else { - wp_send_json_error( __( 'Invalid course ID', 'tutor' ) ); + wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) ); } } @@ -3364,6 +3389,7 @@ public static function course_status_list() { * * @param int $post_ID The WordPress post ID of the course. * @param int $product_id The WooCommerce product ID to associate with the course. + * * @return void */ public static function sync_course_with_wc_product( $post_ID, $product_id ) { diff --git a/classes/Course_Filter.php b/classes/Course_Filter.php index 18bb06a558..e013929c59 100644 --- a/classes/Course_Filter.php +++ b/classes/Course_Filter.php @@ -47,6 +47,7 @@ class Course_Filter { * @since 1.0.0 * * @param boolean $register_hook register hook or not. + * * @return void|null */ public function __construct( $register_hook = true ) { @@ -55,7 +56,7 @@ public function __construct( $register_hook = true ) { } add_action( 'wp_ajax_tutor_course_filter_ajax', array( $this, 'load_listing' ), 10, 0 ); add_action( 'wp_ajax_nopriv_tutor_course_filter_ajax', array( $this, 'load_listing' ), 10, 0 ); - add_filter( 'term_link', __CLASS__ . '::filter_course_category_term_link', 10, 3 ); + add_filter( 'term_link', array( $this, 'filter_course_category_term_link' ), 10, 3 ); } /** @@ -64,7 +65,7 @@ public function __construct( $register_hook = true ) { * @since 1.0.0 * * @param mixed $filters filters. - * @param boolean $return_filter return filterd data or not. + * @param boolean $return_filter return filtered data or not. * @return mixed */ public function load_listing( $filters = null, $return_filter = false ) { @@ -122,26 +123,14 @@ public function load_listing( $filters = null, $return_filter = false ) { ); $post_ids_array = tutils()->array_get( 'tutor-course-filter-post-ids', $sanitized_post, array() ); - - $post_ids_array = array_map( - function ( $post_id ) { - return (int) $post_id; - }, - $post_ids_array - ); + $post_ids_array = array_map( 'intval', $post_ids_array ); if ( count( $post_ids_array ) ) { $args['post__in'] = $post_ids_array; } $exclude_ids_array = tutils()->array_get( 'tutor-course-filter-exclude-ids', $sanitized_post, array() ); - - $exclude_ids_array = array_map( - function ( $exclude_id ) { - return (int) $exclude_id; - }, - $exclude_ids_array - ); + $exclude_ids_array = array_map( 'intval', $exclude_ids_array ); if ( count( $exclude_ids_array ) ) { $args['post__not_in'] = $exclude_ids_array; @@ -152,12 +141,7 @@ function ( $exclude_id ) { $term_array = tutils()->array_get( 'tutor-course-filter-' . $taxonomy, $sanitized_post, array() ); ! is_array( $term_array ) ? $term_array = array( $term_array ) : 0; - $term_array = array_filter( - $term_array, - function( $term_id ) { - return is_numeric( $term_id ); - } - ); + $term_array = array_filter( $term_array, fn ( $term_id ) => is_numeric( $term_id ) ); if ( count( $term_array ) > 0 ) { $tax_query = array( @@ -288,6 +272,7 @@ private function sort_terms_hierarchically( $terms, $parent_id = 0 ) { * * @param array $terms term list. * @param string $taxonomy taxonomy name. + * * @return void */ private function render_terms_hierarchically( $terms, $taxonomy ) { @@ -312,6 +297,7 @@ private function render_terms_hierarchically( $terms, $taxonomy ) { * @since 1.0.0 * * @param string $taxonomy taxonomy name. + * * @return void */ public function render_terms( $taxonomy ) { diff --git a/classes/Course_List.php b/classes/Course_List.php index 6d0d0051d0..9400b7b372 100644 --- a/classes/Course_List.php +++ b/classes/Course_List.php @@ -10,12 +10,11 @@ namespace TUTOR; +defined( 'ABSPATH' ) || exit; + use Tutor\Helpers\QueryHelper; use Tutor\Models\CourseModel; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} /** * Course List class * @@ -41,25 +40,26 @@ class Course_List { * Constructor * * @return void + * * @since 2.0.0 */ public function __construct() { /** * Handle bulk action * - * @since v2.0.0 + * @since 2.0.0 */ add_action( 'wp_ajax_tutor_course_list_bulk_action', array( $this, 'course_list_bulk_action' ) ); /** * Handle ajax request for updating course status * - * @since v2.0.0 + * @since 2.0.0 */ add_action( 'wp_ajax_tutor_change_course_status', array( $this, 'tutor_change_course_status' ) ); /** * Handle ajax request for delete course * - * @since v2.0.0 + * @since 2.0.0 */ add_action( 'wp_ajax_tutor_course_delete', array( $this, 'tutor_course_delete' ) ); } @@ -82,8 +82,9 @@ public function __get( $name ) { /** * Prepare bulk actions that will show on dropdown options * - * @return array * @since 2.0.0 + * + * @return array */ public function prepare_bulk_actions(): array { $actions = array( @@ -95,22 +96,17 @@ public function prepare_bulk_actions(): array { $active_tab = Input::get( 'data', '' ); - if ( 'trash' === $active_tab ) { + if ( CourseModel::STATUS_TRASH === $active_tab ) { array_push( $actions, $this->bulk_action_delete() ); } - if ( 'trash' !== $active_tab ) { + if ( CourseModel::STATUS_TRASH !== $active_tab ) { array_push( $actions, $this->bulk_action_trash() ); } - if ( ! current_user_can( 'administrator' ) ) { + if ( ! User::is_admin() ) { $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' ) && current_user_can( 'edit_tutor_course' ); if ( ! $can_trash_post ) { - $actions = array_filter( - $actions, - function ( $val ) { - return 'trash' !== $val['value']; - } - ); + $actions = array_filter( $actions, fn ( $val ) => CourseModel::STATUS_TRASH !== $val['value'] ); } } return apply_filters( 'tutor_course_bulk_actions', $actions ); @@ -119,14 +115,14 @@ function ( $val ) { /** * Available tabs that will visible on the right side of page navbar * + * @since 2.0.0 + * * @param string $category_slug category slug. * @param integer $course_id course ID. * @param string $date selected date | optional. * @param string $search search by user name or email | optional. * * @return array - * - * @since v2.0.0 */ public function tabs_key_value( $category_slug, $course_id, $date, $search ): array { $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() ); @@ -200,6 +196,8 @@ public function tabs_key_value( $category_slug, $course_id, $date, $search ): ar * Count courses by status & filters * Count all | min | published | pending | draft * + * @since 2.0.0 + * * @param string $status | required. * @param string $category_slug course category | optional. * @param string $course_id selected course id | optional. @@ -207,8 +205,6 @@ public function tabs_key_value( $category_slug, $course_id, $date, $search ): ar * @param string $search_term search by user name or email | optional. * * @return int - * - * @since 2.0.0 */ protected static function count_course( string $status, $category_slug = '', $course_id = '', $date = '', $search_term = '' ): int { $user_id = get_current_user_id(); @@ -235,9 +231,9 @@ protected static function count_course( string $status, $category_slug = '', $co $date_filter = sanitize_text_field( $date ); - $year = date( 'Y', strtotime( $date_filter ) ); - $month = date( 'm', strtotime( $date_filter ) ); - $day = date( 'd', strtotime( $date_filter ) ); + $year = gmdate( 'Y', strtotime( $date_filter ) ); + $month = gmdate( 'm', strtotime( $date_filter ) ); + $day = gmdate( 'd', strtotime( $date_filter ) ); // Add date query. if ( '' !== $date_filter ) { @@ -278,8 +274,9 @@ protected static function count_course( string $status, $category_slug = '', $co /** * Handle bulk action for enrollment cancel | delete * - * @return void * @since 2.0.0 + * + * @return void */ public function course_list_bulk_action() { @@ -289,13 +286,13 @@ public function course_list_bulk_action() { $bulk_ids = Input::post( 'bulk-ids', '' ); // Check if user is privileged. - if ( ! current_user_can( 'administrator' ) ) { + if ( ! User::is_admin() ) { $course_ids = explode( ',', $bulk_ids ); if ( current_user_can( 'edit_tutor_course' ) ) { $can_publish_course = tutor_utils()->get_option( 'instructor_can_publish_course' ); - if ( 'publish' === $action && ! $can_publish_course ) { + if ( CourseModel::STATUS_PUBLISH === $action && ! $can_publish_course ) { wp_send_json_error( tutor_utils()->error_message() ); } } else { @@ -350,18 +347,19 @@ function ( $course_id ) { /** * Handle ajax request for updating course status * - * @return void * @since 2.0.0 + * + * @return void */ public static function tutor_change_course_status() { tutor_utils()->checking_nonce(); $status = Input::post( 'status' ); - $id = Input::post( 'id' ); + $id = Input::post( 'id', 0, Input::TYPE_INT ); $course = get_post( $id ); // Check if user is privileged. - if ( ! current_user_can( 'administrator' ) ) { + if ( ! User::is_admin() ) { if ( ! tutor_utils()->can_user_edit_course( get_current_user_id(), $course->ID ) ) { wp_send_json_error( tutor_utils()->error_message() ); @@ -370,11 +368,11 @@ public static function tutor_change_course_status() { $can_delete_course = tutor_utils()->get_option( 'instructor_can_delete_course' ); $can_publish_course = tutor_utils()->get_option( 'instructor_can_publish_course' ); - if ( 'publish' === $status && ! $can_publish_course ) { + if ( CourseModel::STATUS_PUBLISH === $status && ! $can_publish_course ) { wp_send_json_error( tutor_utils()->error_message() ); } - if ( 'trash' === $status && $can_delete_course ) { + if ( CourseModel::STATUS_TRASH === $status && $can_delete_course ) { $args = array( 'ID' => $id, 'post_status' => $status, @@ -439,9 +437,11 @@ public static function tutor_course_delete() { /** * Execute bulk delete action * + * @since 2.0.0 + * * @param string $bulk_ids ids that need to update. + * * @return bool - * @since 2.0.0 */ public static function bulk_delete_course( $bulk_ids ): bool { $bulk_ids = explode( ',', sanitize_text_field( $bulk_ids ) ); @@ -456,12 +456,12 @@ public static function bulk_delete_course( $bulk_ids ): bool { /** * Update course status * + * @since 2.0.0 + * * @param string $status for updating course status. * @param string $bulk_ids comma separated ids. * * @return bool - * - * @since 2.0.0 */ public static function update_course_status( string $status, $bulk_ids ): bool { global $wpdb; @@ -472,7 +472,7 @@ public static function update_course_status( string $status, $bulk_ids ): bool { $ids = array_map( 'intval', explode( ',', $bulk_ids ) ); $in_clause = QueryHelper::prepare_in_clause( $ids ); - $update = $wpdb->query( + $wpdb->query( $wpdb->prepare( "UPDATE {$post_table} SET post_status = %s WHERE ID IN ($in_clause)", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $status @@ -483,57 +483,13 @@ public static function update_course_status( string $status, $bulk_ids ): bool { } /** - * Get course enrollment list with student info + * Check course is public or not * - * @param int $course_id int | required. - * @return array - * @since 2.0.0 - */ - public static function course_enrollments_with_student_details( int $course_id ) { - global $wpdb; - $course_id = sanitize_text_field( $course_id ); - $course_completed = 0; - $course_inprogress = 0; - - $enrollments = $wpdb->get_results( - $wpdb->prepare( - "SELECT enroll.ID AS enroll_id, enroll.post_author AS enroll_author, user.*, course.ID AS course_id - FROM {$wpdb->posts} AS enroll - LEFT JOIN {$wpdb->users} AS user ON user.ID = enroll.post_author - LEFT JOIN {$wpdb->posts} AS course ON course.ID = enroll.post_parent - WHERE enroll.post_type = %s - AND enroll.post_status = %s - AND enroll.post_parent = %d - ", - 'tutor_enrolled', - 'completed', - $course_id - ) - ); - - foreach ( $enrollments as $enrollment ) { - $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $enrollment->enroll_author ); - if ( 100 == $course_progress ) { - $course_completed++; - } else { - $course_inprogress++; - } - } - - return array( - 'enrollments' => $enrollments, - 'total_completed' => $course_completed, - 'total_inprogress' => $course_inprogress, - 'total_enrollments' => count( $enrollments ), - ); - } - - /** - * Check wheather course is public or not + * @since 1.0.0 * * @param integer $course_id course id to check with. + * * @return boolean true if public otherwise false. - * @since 1.0.0 */ public static function is_public( int $course_id ): bool { $is_public = get_post_meta( $course_id, '_tutor_is_public_course', true ); diff --git a/classes/Course_Settings_Tabs.php b/classes/Course_Settings_Tabs.php index 4a79b46f51..d298a29590 100644 --- a/classes/Course_Settings_Tabs.php +++ b/classes/Course_Settings_Tabs.php @@ -15,7 +15,7 @@ } /** - * Course Settings Tabls Class + * Course Settings Tabs Class * * @since 2.0.0 */ diff --git a/classes/Custom_Validation.php b/classes/Custom_Validation.php index 2ebdd16e59..b55edf5e2d 100644 --- a/classes/Custom_Validation.php +++ b/classes/Custom_Validation.php @@ -11,7 +11,7 @@ namespace TUTOR; /** - * Custom Valaidation Trait + * Custom Validation Trait * * @since 2.0.0 */ @@ -26,7 +26,7 @@ trait Custom_Validation { * @return bool */ public function validate_order( $order ) { - return in_array( strtolower( $order ), array( 'asc', 'desc' ) ); + return in_array( strtolower( $order ), array( 'asc', 'desc' ), true ); } } diff --git a/classes/Instructors_List.php b/classes/Instructors_List.php index e357f15127..8d6fed4b13 100644 --- a/classes/Instructors_List.php +++ b/classes/Instructors_List.php @@ -10,9 +10,7 @@ namespace TUTOR; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; use TUTOR\Students_List; use TUTOR\Backend_Page_Trait; @@ -129,9 +127,10 @@ public function tabs_key_value( $search = '', $course_id = '', $date = '' ): arr * Prepare bulk actions that will show on dropdown options * * @since 2.0.0 + * * @return array */ - public function prpare_bulk_actions(): array { + public function prepare_bulk_actions(): array { $actions = array( $this->bulk_action_default(), $this->bulk_action_approved(), diff --git a/classes/Lesson.php b/classes/Lesson.php index 39a47c95f2..7182f4534f 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -148,6 +148,11 @@ public function ajax_single_course_lesson_load_more() { } if ( 'tutor_create_lesson_comment' === Input::post( 'action' ) && strlen( $comment ) > 0 ) { + $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id ); + if ( ! tutor_utils()->is_enrolled( $course_id ) ) { + wp_send_json_error( __( 'You must be enrolled to comment', 'tutor' ) ); + } + $comment_data = array( 'comment_content' => $comment, 'comment_post_ID' => $lesson_id, @@ -375,28 +380,18 @@ public function save_lesson_meta( $post_ID ) { */ public function ajax_lesson_details() { if ( ! tutor_utils()->is_nonce_verified() ) { - $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST ); + $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) ); } $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT ); $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT ); - if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) { - $this->json_response( - tutor_utils()->error_message(), - null, - HttpHelper::STATUS_FORBIDDEN - ); + if ( ! $topic_id || ! $lesson_id ) { + $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) ); } - if ( 0 !== $lesson_id ) { - if ( ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) { - $this->json_response( - tutor_utils()->error_message(), - null, - HttpHelper::STATUS_FORBIDDEN - ); - } + if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) || ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) { + $this->response_bad_request( tutor_utils()->error_message() ); } $data = LessonModel::get_lesson_details( $lesson_id ); @@ -418,7 +413,7 @@ public function ajax_lesson_details() { */ public function ajax_save_lesson() { if ( ! tutor_utils()->is_nonce_verified() ) { - $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST ); + $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) ); } /** @@ -429,22 +424,14 @@ public function ajax_save_lesson() { */ add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 ); - $is_update = false; - $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT ); $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT ); - if ( $lesson_id ) { - $is_update = true; + if ( ! $topic_id || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) { + $this->response_bad_request( tutor_utils()->error_message() ); } - if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) { - $this->json_response( - tutor_utils()->error_message(), - null, - HttpHelper::STATUS_FORBIDDEN - ); - } + $is_update = $lesson_id > 0; $title = Input::post( 'title' ); $description = Input::post( 'description', '', Input::TYPE_KSES_POST ); @@ -536,12 +523,8 @@ public function ajax_delete_lesson() { $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT ); - if ( ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) { - $this->json_response( - tutor_utils()->error_message(), - null, - HttpHelper::STATUS_FORBIDDEN - ); + if ( ! $lesson_id || ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) { + $this->response_bad_request( tutor_utils()->error_message() ); } $content = __( 'Lesson', 'tutor' ); @@ -601,12 +584,11 @@ public function mark_lesson_complete() { if ( 'tutor_complete_lesson' !== Input::post( 'tutor_action' ) ) { return; } - // Checking nonce. + tutor_utils()->checking_nonce(); $user_id = get_current_user_id(); - // TODO: need to show view if not signed_in. if ( ! $user_id ) { die( esc_html__( 'Please Sign-In', 'tutor' ) ); } @@ -617,6 +599,12 @@ public function mark_lesson_complete() { return; } + $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id ); + + if ( ! $course_id || ! tutor_utils()->is_enrolled( $course_id ) ) { + die( esc_html( tutor_utils()->error_message() ) ); + } + $validated = apply_filters( 'tutor_validate_lesson_complete', true, $user_id, $lesson_id ); if ( ! $validated ) { return; @@ -643,18 +631,19 @@ public function tutor_render_lesson_content() { tutor_utils()->checking_nonce(); $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT ); + if ( ! $lesson_id ) { + $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) ); + } - $ancestors = get_post_ancestors( $lesson_id ); - $course_id = ! empty( $ancestors ) ? array_pop( $ancestors ) : $lesson_id; + $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id ); // Course must be public or current user must be enrolled to access this lesson. - if ( get_post_meta( $course_id, '_tutor_is_public_course', true ) !== 'yes' && ! EnrollmentModel::is_enrolled( $course_id ) ) { + if ( ! Course_List::is_public( $course_id ) && ! EnrollmentModel::is_enrolled( $course_id ) ) { - $is_admin = tutor_utils()->has_user_role( 'administrator' ); - $allowed = $is_admin ? true : tutor_utils()->is_instructor_of_this_course( get_current_user_id(), $course_id ); + $allowed = User::is_admin() ? true : tutor_utils()->is_instructor_of_this_course( get_current_user_id(), $course_id ); if ( ! $allowed ) { - http_response_code( 400 ); + $this->response_bad_request( tutor_utils()->error_message() ); exit; } } @@ -723,15 +712,22 @@ public function tutor_lesson_completed_after( $content_id ) { */ public function ajax_reply_lesson_comment() { tutor_utils()->checking_nonce(); - $comment = Input::post( 'comment', '', Input::TYPE_KSES_POST ); + $comment = Input::post( 'comment', '', Input::TYPE_TEXTAREA ); if ( 0 === strlen( $comment ) ) { wp_send_json_error(); return; } + $lesson_id = Input::post( 'comment_post_ID', 0, Input::TYPE_INT ); + $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id ); + if ( ! tutor_utils()->is_enrolled( $course_id ) ) { + wp_send_json_error( __( 'You must be enrolled to comment', 'tutor' ) ); + return; + } + $comment_data = array( 'comment_content' => $comment, - 'comment_post_ID' => Input::post( 'comment_post_ID', 0, Input::TYPE_INT ), + 'comment_post_ID' => $lesson_id, 'comment_parent' => Input::post( 'comment_parent', 0, Input::TYPE_INT ), ); diff --git a/classes/Quiz.php b/classes/Quiz.php index 120751b9db..b6d75af3ec 100644 --- a/classes/Quiz.php +++ b/classes/Quiz.php @@ -633,11 +633,10 @@ public static function quiz_attempt( int $course_id, int $quiz_id, int $user_id, * @return void */ public function answering_quiz() { - - if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) { + if ( 'tutor_answering_quiz_question' !== Input::post( 'tutor_action' ) ) { return; } - // submit quiz attempts. + self::tutor_quiz_attempt_submit(); wp_safe_redirect( get_the_permalink() ); @@ -649,12 +648,13 @@ public function answering_quiz() { * * @since 1.9.6 * - * @return JSON response + * @return void JSON response */ public function tutor_quiz_abandon() { - if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) { + if ( 'tutor_answering_quiz_question' !== Input::post( 'tutor_action' ) ) { return; } + tutor_utils()->checking_nonce(); // submit quiz attempts. if ( self::tutor_quiz_attempt_submit() ) { @@ -664,28 +664,48 @@ public function tutor_quiz_abandon() { } } + /** + * Validate quiz attempt + * + * @since 3.9.13 + * + * @param int $attempt_id attempt id. + * @param int $user_id user id. + * + * @return object|false attempt object if valid otherwise false + */ + private static function validate_attempt( $attempt_id, $user_id ) { + $attempt = tutor_utils()->get_attempt( $attempt_id ); + + if ( ! $attempt || ! is_object( $attempt ) || (int) $attempt->user_id !== (int) $user_id ) { + return false; + } + + return $attempt; + } + /** * This is a unified method for handling normal quiz submit or abandon submit * It will handle ajax or normal form submit and can be used with different hooks * * @since 1.9.6 * - * @return true | false + * @return bool true if quiz attempt submit successfully otherwise false */ public static function tutor_quiz_attempt_submit() { - // Check logged in. if ( ! is_user_logged_in() ) { die( 'Please sign in to do this operation' ); } - // Check nonce. tutor_utils()->checking_nonce(); - // Prepare attempt info. $user_id = get_current_user_id(); $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT ); - $attempt = tutor_utils()->get_attempt( $attempt_id ); - $course_id = CourseModel::get_course_by_quiz( $attempt->quiz_id )->ID; + $attempt = self::validate_attempt( $attempt_id, $user_id ); + + if ( ! $attempt ) { + die( 'Operation not allowed, attempt not found or permission denied' ); + } if ( QuizModel::ATTEMPT_TIMEOUT === $attempt->attempt_status ) { return false; @@ -695,11 +715,7 @@ public static function tutor_quiz_attempt_submit() { $attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore $attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array(); - // Check if has access to the attempt. - if ( ! $attempt || $user_id != $attempt->user_id ) { - die( 'Operation not allowed, attempt not found or permission denied' ); - } - self::manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id ); + self::manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $attempt->course_id, $user_id ); return true; } @@ -719,23 +735,26 @@ public static function tutor_quiz_attempt_submit() { * @return void */ public static function manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id ) { + if ( ! is_array( $attempt_answers ) || ! self::validate_attempt( $attempt_id, $user_id ) ) { + return; + } + global $wpdb; // Before hook. do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id ); // Single quiz can have multiple question. So multiple answer should be saved. - foreach ( $attempt_answers as $attempt_id => $attempt_answer ) { + foreach ( $attempt_answers as $posted_attempt_id => $attempt_answer ) { + if ( ! self::validate_attempt( $posted_attempt_id, $user_id ) ) { + continue; + } + // Get total marks of all question comes. $question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer ); - $question_ids = array_filter( - $question_ids, - function ( $id ) { - return (int) $id; - } - ); + $question_ids = array_filter( $question_ids, fn ( $id ) => is_numeric( $id ) && intval( $id ) > 0 ); // Calculate and set the total marks in attempt table for this question. - if ( is_array( $question_ids ) && count( $question_ids ) ) { + if ( tutor_utils()->count( $question_ids ) ) { $question_ids_string = QueryHelper::prepare_in_clause( $question_ids ); // Get total marks of the questions from question table. @@ -789,13 +808,8 @@ function ( $id ) { } elseif ( 'multiple_choice' === $question_type ) { $given_answer = (array) ( $answers ); + $given_answer = array_filter( $given_answer, fn ( $id ) => is_numeric( $id ) && intval( $id ) > 0 ); - $given_answer = array_filter( - $given_answer, - function ( $id ) { - return is_numeric( $id ) && $id > 0; - } - ); $get_original_answers = (array) $wpdb->get_col( $wpdb->prepare( "SELECT @@ -852,7 +866,7 @@ function ( $ans ) { /** * Compare answer's by making both case-insensitive. */ - if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) { + if ( strtolower( $given_answer ) === strtolower( $gap_answer ) ) { $is_answer_was_correct = true; } } elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) { diff --git a/classes/QuizBuilder.php b/classes/QuizBuilder.php index 3b5d05ff3c..2d72e597cf 100644 --- a/classes/QuizBuilder.php +++ b/classes/QuizBuilder.php @@ -345,6 +345,19 @@ public function validate_payload( $payload ) { $errors = array_merge( $errors, $validation->errors ); } + if ( isset( $payload['ID'] ) && is_numeric( $payload['ID'] ) ) { + if ( ! current_user_can( 'edit_post', $payload['ID'] ) ) { + $success = false; + $errors['permission'][] = __( 'You do not have permission to edit this quiz', 'tutor' ); + } else { + $quiz = get_post( $payload['ID'] ); + if ( ! $quiz || tutor()->quiz_post_type !== $quiz->post_type ) { + $success = false; + $errors['ID'][] = __( 'Invalid quiz id provided', 'tutor' ); + } + } + } + foreach ( $payload['questions'] as $question ) { if ( ! isset( $question[ self::TRACKING_KEY ] ) ) { $success = false; diff --git a/classes/Utils.php b/classes/Utils.php index 5c23a36a21..fd15bc7ce7 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -7329,14 +7329,14 @@ public function get_cover_photo_url( $user_id ) { } /** - * Return the course ID(s) by lession, quiz, answer etc. + * Return the course ID by lesson, quiz, answer etc. * * @since 1.7.9 * - * @param string $content content like lession, quiz, answer etc. + * @param string $content content like lesson, quiz, answer etc. * @param int $object_id object id. * - * @return int|int[] + * @return int */ public function get_course_id_by( $content, $object_id ) { $cache_key = "tutor_get_course_id_by_{$content}_{$object_id}"; @@ -7583,10 +7583,7 @@ public function has_enrolled_content_access( $content, $object_id = 0, $user_id $user_id = $this->get_user_id( $user_id ); $object_id = $this->get_post_id( $object_id ); - $course_id = Input::get( 'course', 0, Input::TYPE_INT ); - if ( ! $course_id ) { - $course_id = $this->get_course_id_by( $content, $object_id ); - } + $course_id = $this->get_course_id_by( $content, $object_id ); do_action( 'tutor_before_enrolment_check', $course_id, $user_id ); diff --git a/readme.txt b/readme.txt index c257dd498e..b2c5a12af6 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: lms, course, elearning, education, learning management system Requires at least: 5.3 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 3.9.12 +Stable tag: 3.9.13 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -488,6 +488,9 @@ New: Lots of Micro-interactions with animations and sound effects to make lesson New: A new button “view as student” has been added to allow admin & instructor to change the dashboard view. New: Live classes, a new dashboard menu has been added to manage the Google & Zoom meetings from the same page. (Pro) New: Redesigned the Instructor dashboard with earnings overview, course stats, and student activity. += 3.9.13 - Jun 17, 2026 + +Update: Patched multiple security vulnerabilities. = 3.9.12 - Jun 09, 2026 diff --git a/templates/single/lesson/content.php b/templates/single/lesson/content.php index 43096e24f3..4b98680038 100644 --- a/templates/single/lesson/content.php +++ b/templates/single/lesson/content.php @@ -78,7 +78,7 @@ $json_data['lesson_completed'] = tutor_utils()->is_completed_lesson( $content_id, get_current_user_id() ) !== false; $json_data['is_enrolled'] = EnrollmentModel::is_enrolled( $course_id, get_current_user_id() ) !== false; ?> - +