From e39dadc15a361746935a0ef1e47313909a41a785 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Tue, 16 Jun 2026 12:23:12 +0600 Subject: [PATCH 01/25] fix: quiz attempt security issue --- classes/Quiz.php | 76 ++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/classes/Quiz.php b/classes/Quiz.php index 6ca2941c78..9377f54f27 100644 --- a/classes/Quiz.php +++ b/classes/Quiz.php @@ -422,11 +422,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() ); @@ -438,12 +437,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() ) { @@ -453,28 +453,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; @@ -484,11 +504,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; } @@ -508,23 +524,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. @@ -584,13 +603,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 @@ -647,7 +661,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 ) { From df5aea3d433ffeade4bcc2736d726f7bf7bbc4ba Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Tue, 16 Jun 2026 12:24:06 +0600 Subject: [PATCH 02/25] fix: remove redundant user ID parameter from quiz attempt documentation --- classes/Quiz.php | 1 - 1 file changed, 1 deletion(-) diff --git a/classes/Quiz.php b/classes/Quiz.php index 9377f54f27..bd1e2ffe11 100644 --- a/classes/Quiz.php +++ b/classes/Quiz.php @@ -1238,7 +1238,6 @@ public function attempt_delete() { * @since 3.8.1 * * @param int $course_id The ID of the course. - * @param int $user_id The ID of the user. * * @return array Returns an array of quiz attempt objects with their answers, or an empty array on error. */ From 017c6f9169f7ade49ebbea433e007947fac4f41d Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Tue, 16 Jun 2026 13:23:53 +0600 Subject: [PATCH 03/25] fix: security issue fix for quiz update --- classes/QuizBuilder.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/classes/QuizBuilder.php b/classes/QuizBuilder.php index e56dbe7de1..c463b7494b 100644 --- a/classes/QuizBuilder.php +++ b/classes/QuizBuilder.php @@ -257,6 +257,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; From 8d468f3037ff1b4b3de91251da589172ed448a31 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Tue, 16 Jun 2026 17:59:31 +0600 Subject: [PATCH 04/25] fix: create and reply lesson comment security issue --- classes/Lesson.php | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/classes/Lesson.php b/classes/Lesson.php index 8e3b9fa8f3..65cff57a1b 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -96,11 +96,19 @@ public function __construct( $register_hooks = true ) { */ public function tutor_single_course_lesson_load_more() { tutor_utils()->checking_nonce(); - $comment = Input::post( 'comment', '', Input::TYPE_KSES_POST ); + + $comment = Input::post( 'comment', '', Input::TYPE_TEXTAREA ); + $lesson_id = Input::post( 'comment_post_ID', 0, Input::TYPE_INT ); + 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' => Input::post( 'comment_post_ID', 0, Input::TYPE_INT ), + 'comment_post_ID' => $lesson_id, 'comment_parent' => Input::post( 'comment_parent', 0, Input::TYPE_INT ), ); self::create_comment( $comment_data ); @@ -524,18 +532,26 @@ public function tutor_lesson_completed_after( $content_id ) { */ public function 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 ), ); - $comment_id = self::create_comment( $comment_data ); + + $comment_id = self::create_comment( $comment_data ); if ( false === $comment_id ) { wp_send_json_error(); return; From 6d4bbfa80374827f8beed3c4eb4ce2b3a4374812 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Wed, 17 Jun 2026 11:42:31 +0600 Subject: [PATCH 05/25] fix: update version to 3.9.13 and patch multiple security vulnerabilities --- readme.txt | 6 +++++- tutor.php | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/readme.txt b/readme.txt index 385f4580b0..5be77bae94 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 @@ -319,6 +319,10 @@ Tutor LMS allows you to offer certificates to your students upon course completi == Changelog == += 3.9.13 - Jun 17, 2026 + +Update: Patched multiple security vulnerabilities. + = 3.9.12 - Jun 09, 2026 Update: Added compatibility support for PHP 8.5 diff --git a/tutor.php b/tutor.php index 51bd625959..04690ed0d2 100644 --- a/tutor.php +++ b/tutor.php @@ -4,7 +4,7 @@ * Plugin URI: https://tutorlms.com * Description: Tutor is a complete solution for creating a Learning Management System in WordPress way. It can help you to create small to large scale online education site very conveniently. Power features like report, certificate, course preview, private file sharing make Tutor a robust plugin for any educational institutes. * Author: Themeum - * Version: 3.9.12 + * Version: 3.9.13 * Author URI: https://themeum.com * Requires PHP: 7.4 * Requires at least: 5.3 @@ -26,7 +26,7 @@ * * @since 1.0.0 */ -define( 'TUTOR_VERSION', '3.9.12' ); +define( 'TUTOR_VERSION', '3.9.13' ); define( 'TUTOR_FILE', __FILE__ ); /** From 6cd3557db2e71a048dd929f9473282b3b5ea13c3 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Wed, 17 Jun 2026 11:54:32 +0600 Subject: [PATCH 06/25] removed unused code --- assets/react/v3/shared/utils/localStorage.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/assets/react/v3/shared/utils/localStorage.ts b/assets/react/v3/shared/utils/localStorage.ts index f5d738e6cc..62bb98a707 100644 --- a/assets/react/v3/shared/utils/localStorage.ts +++ b/assets/react/v3/shared/utils/localStorage.ts @@ -1,7 +1,4 @@ import type { LocalStorageKeys } from '@TutorShared/config/constants'; -import { EventEmitter } from 'events'; - -export const localStorageEventEmitter = new EventEmitter(); export const setToLocalStorage = (key: LocalStorageKeys, value: string) => { localStorage.setItem(key, value); From 7ba626be2428f087e4d3f4509e4b5db0eba1d0b5 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Wed, 17 Jun 2026 12:31:42 +0600 Subject: [PATCH 07/25] cs fix and consistent permission checking added --- classes/Addons.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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() { From f72ec783b80eff2a51958ac6eb04d7204413dbe1 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 10:24:49 +0600 Subject: [PATCH 08/25] fix: streamline ABSPATH check in Admin and Ajax classes --- classes/Admin.php | 6 ++---- classes/Ajax.php | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/classes/Admin.php b/classes/Admin.php index 0e83c22b15..07186d983c 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 954648efe2..c8a93a4af6 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -10,9 +10,7 @@ namespace TUTOR; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; use Tutor\Helpers\HttpHelper; use Tutor\Models\LessonModel; @@ -27,6 +25,7 @@ class Ajax { use JsonResponse; const LOGIN_ERRORS_TRANSIENT_KEY = 'tutor_login_errors'; + /** * Constructor * From 84737a0beef1b46ef484129d2976a4d9b8dbf2dc Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 11:53:36 +0600 Subject: [PATCH 09/25] refactor: update login transient expiration and standardize nonce verification in Ajax handler --- classes/Ajax.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/classes/Ajax.php b/classes/Ajax.php index c8a93a4af6..0642287981 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -394,9 +394,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 */ @@ -409,12 +407,11 @@ 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; } - //phpcs:disable WordPress.Security.NonceVerification.Missing /** * No sanitization/wp_unslash needed for log & pwd since WordPress @@ -424,10 +421,12 @@ public function process_tutor_login() { * * @see https://developer.wordpress.org/reference/functions/wp_signon/ */ + //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( @@ -484,7 +483,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 ); } } From 523c90dca37d7a21366586f417f426c76ec625e3 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 12:35:31 +0600 Subject: [PATCH 10/25] Security Fix: Prevent arbitrary access to course content --- classes/Utils.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/classes/Utils.php b/classes/Utils.php index 6776d870f7..2d9b7b4d2a 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -8070,10 +8070,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 ); From 00a1852861913372549b06b5bb5d2fd904174f4b Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 13:06:58 +0600 Subject: [PATCH 11/25] fix: restrict lesson completion tracking to enrolled users and remove redundant nopriv video playback action --- assets/react/front/tutor-front.js | 5 +++++ classes/Ajax.php | 11 +---------- templates/single/lesson/content.php | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/assets/react/front/tutor-front.js b/assets/react/front/tutor-front.js index 53f5b84d9f..ccd6a49ff7 100644 --- a/assets/react/front/tutor-front.js +++ b/assets/react/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/Ajax.php b/classes/Ajax.php index 0642287981..053cbcd974 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -39,7 +39,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' ) ); @@ -71,6 +70,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() { @@ -114,15 +114,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 * diff --git a/templates/single/lesson/content.php b/templates/single/lesson/content.php index 30d8f2535d..122c109636 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'] = tutor_utils()->is_enrolled( $course_id, get_current_user_id() ) !== false; ?> - +
From ae52ff0146c6fb6d4a8e8ba1bfddd93fc67ca72f Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 13:15:30 +0600 Subject: [PATCH 12/25] fix: correct spelling of announcement in Ajax class methods --- classes/Ajax.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/classes/Ajax.php b/classes/Ajax.php index 053cbcd974..b512298751 100644 --- a/classes/Ajax.php +++ b/classes/Ajax.php @@ -57,8 +57,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' ) ); } @@ -482,9 +482,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(); @@ -492,7 +493,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' ) ) ); } @@ -565,9 +566,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' ); From 38aa65f56101192764b84e1ae4c33070b0b42904 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 13:17:32 +0600 Subject: [PATCH 13/25] fix: improve code readability by standardizing comments and formatting in Announcements class --- classes/Announcements.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/classes/Announcements.php b/classes/Announcements.php index 87a2802533..f143e0eb14 100644 --- a/classes/Announcements.php +++ b/classes/Announcements.php @@ -10,11 +10,10 @@ namespace TUTOR; +defined( 'ABSPATH' ) || exit; + use Tutor\Helpers\QueryHelper; -if ( ! defined( 'ABSPATH' ) ) { - exit; -} /** * Announcements class * @@ -40,6 +39,7 @@ class Announcements { * Constructor * * @since 1.0.0 + * * @return void */ public function __construct() { @@ -71,6 +71,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 { @@ -85,6 +86,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() { @@ -115,7 +117,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 */ From 94b2a78485632e6eb19ec4c157d8257e47b2dc3d Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 13:29:34 +0600 Subject: [PATCH 14/25] fix: standardize comments and improve code readability in Course_Filter class --- classes/Course_Filter.php | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) 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 ) { From 49bd166cf989737dd60e34c89e341f98c90875c8 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 15:19:22 +0600 Subject: [PATCH 15/25] fix: improve code readability and standardize comments in Course_List class --- classes/Course_List.php | 80 +++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/classes/Course_List.php b/classes/Course_List.php index 6d0d0051d0..072c95e3a8 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 @@ -485,9 +485,11 @@ public static function update_course_status( string $status, $bulk_ids ): bool { /** * Get course enrollment list with student info * + * @since 2.0.0 + * * @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; @@ -529,11 +531,13 @@ public static function course_enrollments_with_student_details( int $course_id ) } /** - * Check wheather course is public or not + * Check 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 ); From 8bbecdfd55095c55e66eb6ac588f669d49272d20 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 15:20:06 +0600 Subject: [PATCH 16/25] removed unused method --- classes/Course_List.php | 48 ----------------------------------------- 1 file changed, 48 deletions(-) diff --git a/classes/Course_List.php b/classes/Course_List.php index 072c95e3a8..9400b7b372 100644 --- a/classes/Course_List.php +++ b/classes/Course_List.php @@ -482,54 +482,6 @@ public static function update_course_status( string $status, $bulk_ids ): bool { return true; } - /** - * Get course enrollment list with student info - * - * @since 2.0.0 - * - * @param int $course_id int | required. - * - * @return array - */ - 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 course is public or not * From 4c06d9d00d65065d59447a9f4b8a03aa82ab16a9 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 15:21:32 +0600 Subject: [PATCH 17/25] fix: correct spelling of 'Tabs' in Course_Settings_Tabs class comment --- classes/Course_Settings_Tabs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 */ From 8b938906612d821630302bff80bdf222c10eecb2 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 16:22:55 +0600 Subject: [PATCH 18/25] fix: enhance course ID validation and improve error handling in Course class --- classes/Course.php | 111 +++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/classes/Course.php b/classes/Course.php index d89778ba93..65f08ff449 100644 --- a/classes/Course.php +++ b/classes/Course.php @@ -902,13 +902,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 ); @@ -930,12 +929,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; } @@ -1077,6 +1071,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(); @@ -1167,9 +1165,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 ) { @@ -1248,14 +1250,15 @@ public function get_course_contents( $course_id ) { 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 ); } @@ -1281,12 +1284,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 ); } @@ -1299,7 +1302,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(); @@ -1308,7 +1311,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 ); } @@ -2265,7 +2268,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' ); @@ -2277,7 +2280,7 @@ public function tutor_delete_dashboard_course() { $trash_course = wp_update_post( array( 'ID' => $course_id, - 'post_status' => 'trash', + 'post_status' => CourseModel::STATUS_TRASH, ) ); @@ -2982,9 +2985,9 @@ public function delete_associated_enrollment( $post_id ) { */ public function tutor_reset_course_progress() { tutor_utils()->checking_nonce(); - $course_id = Input::post( 'course_id' ); + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); - if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) { + if ( ! $course_id || ! tutor_utils()->is_enrolled( $course_id ) ) { wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) ); return; } @@ -3028,44 +3031,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) tutor_utils()->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 = tutor_utils()->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) tutor_utils()->is_enrolled( $course_id, $user_id ); + + if ( ! $is_enrolled ) { + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) ); } + } + + $enroll = tutor_utils()->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' ) ); } } From 395b9d830226734d092929d0e32190bc6adf50ad Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Thu, 18 Jun 2026 16:27:53 +0600 Subject: [PATCH 19/25] fix: correct spelling in Custom Validation trait and enforce strict comparison in validate_order method --- classes/Custom_Validation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ); } } From c54a925ad230995117d1ee9c39f4cb292e76ce8b Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Fri, 19 Jun 2026 10:28:37 +0600 Subject: [PATCH 20/25] refactor: fix typo in prepare_bulk_actions method and clean up instructors view template code --- classes/Instructors_List.php | 7 +++---- views/pages/instructors.php | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/classes/Instructors_List.php b/classes/Instructors_List.php index 12e65b9313..812c32b2eb 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; @@ -125,9 +123,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/views/pages/instructors.php b/views/pages/instructors.php index fc00759546..4e6f30379a 100644 --- a/views/pages/instructors.php +++ b/views/pages/instructors.php @@ -9,24 +9,26 @@ * @since 2.0.0 */ -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +defined( 'ABSPATH' ) || exit; use TUTOR\Input; -use TUTOR\Instructors_List; use Tutor\Models\CourseModel; $allowed_subpage = array(); if ( Input::has( 'sub_page' ) ) { $sub_page = Input::get( 'sub_page' ); - if ( in_array( $sub_page, $allowed, true ) ) { + if ( in_array( $sub_page, $allowed_subpage, true ) ) { include_once tutor()->path . "views/pages/{$sub_page}.php"; return; } } +/** + * Instance of Instructors_List class + * + * @var TUTOR\Instructors_List + */ $instructors = tutor_lms()->instructor_list; /** @@ -74,19 +76,19 @@ /** * Navbar data to make nav menu */ -$url = get_pagenum_link(); -$add_insructor_url = $url . '&sub_page=add_new_instructor'; -$navbar_data = array( +$url = get_pagenum_link(); +$add_instructor_url = $url . '&sub_page=add_new_instructor'; +$navbar_data = array( 'page_title' => $instructors->page_title, 'add_button' => true, 'button_title' => __( 'Add New', 'tutor' ), - 'button_url' => $add_insructor_url, + 'button_url' => $add_instructor_url, 'modal_target' => 'tutor-instructor-add-new', ); $filters = array( 'bulk_action' => $instructors->bulk_action, - 'bulk_actions' => $instructors->prpare_bulk_actions(), + 'bulk_actions' => $instructors->prepare_bulk_actions(), 'ajax_action' => 'tutor_instructor_bulk_action', 'filters' => array( array( From c338bdb259b6a2a4f83297a76ad88c1bd71c4d38 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Fri, 19 Jun 2026 11:58:08 +0600 Subject: [PATCH 21/25] refactor: simplify lesson detail and save AJAX handlers using response_bad_request and streamlined permission checks --- classes/Lesson.php | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/classes/Lesson.php b/classes/Lesson.php index 65cff57a1b..18c9dedd71 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -185,28 +185,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 ); @@ -228,7 +218,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' ) ); } /** @@ -239,22 +229,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 ); From 813dd5b9d5f879cbd48cdacd5df39ba5bf89827e Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Fri, 19 Jun 2026 12:07:11 +0600 Subject: [PATCH 22/25] refactor: update lesson management authorization to include ID validation and standardized bad request response --- classes/Lesson.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/classes/Lesson.php b/classes/Lesson.php index 18c9dedd71..0c516f98fc 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -328,12 +328,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' ); From 5477eb7d863c2d36203e17e1cbbfca1e47261b8a Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Fri, 19 Jun 2026 16:15:16 +0600 Subject: [PATCH 23/25] refactor: clean up docblock formatting and optimize ABSTPATH check in Course class --- classes/Course.php | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/classes/Course.php b/classes/Course.php index 09b3a99e85..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; @@ -1286,6 +1284,8 @@ 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(); @@ -1805,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 ) { @@ -2352,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() { @@ -2566,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 ) { @@ -2607,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() { @@ -2641,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 ) { @@ -2657,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 ) { @@ -2673,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 ) { @@ -2689,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 ) { @@ -2705,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 ) { @@ -2768,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() { @@ -2785,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() { @@ -2801,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 ) { @@ -2844,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 ) { @@ -2857,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 ) { @@ -2872,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 ) { @@ -2992,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 ) { @@ -3015,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 ) { @@ -3028,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() { @@ -3053,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 ) { @@ -3086,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() { @@ -3121,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 ) { @@ -3368,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 ) { From ff3886d6780cf349faca0234629596b38eeccdbd Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Fri, 19 Jun 2026 16:26:57 +0600 Subject: [PATCH 24/25] fix: enforce course enrollment validation before lesson completion and restrict get_course_id_by return type to integer --- classes/Lesson.php | 9 +++++++-- classes/Utils.php | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/classes/Lesson.php b/classes/Lesson.php index c1aa5f6313..6ee25a7850 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -584,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' ) ); } @@ -600,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; diff --git a/classes/Utils.php b/classes/Utils.php index d1ba2093b5..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}"; From 4bf6a13ce68c8dab9e1823f158f20083d3243c70 Mon Sep 17 00:00:00 2001 From: "Md.Harun-Ur-Rashid" Date: Mon, 22 Jun 2026 11:54:17 +0600 Subject: [PATCH 25/25] refactor: improve lesson access validation and error handling using standardized methods --- classes/Lesson.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/classes/Lesson.php b/classes/Lesson.php index 6ee25a7850..7182f4534f 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -631,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; } }