{
“analysis”: “Atomic Edge analysis of CVE-2026-6965:nnThis vulnerability is an Insecure Direct Object Reference (IDOR) in the Tutor LMS plugin for WordPress, affecting versions up to and including 3.9.9. The flaw allows an authenticated attacker with instructor-level privileges to perform arbitrary post deletions and other unauthorized operations on any other instructor’s course content. The CVSS score is 5.3 (Medium), but the practical impact can be severe due to cascading data loss.nnThe root cause lies in the `get_course_id_by()` function in `/tutor/classes/Utils.php`. Before the patch, this function unconditionally trusted the `course` GET parameter (line 7829: `$course_id = Input::get( ‘course’, 0, Input::TYPE_INT );`). If this parameter was set, the function returned it immediately without any validation. The returned course ID was then used by `can_user_manage()` to determine whether the current user (instructor) had permission to manage the target content. An attacker could supply a `course` parameter pointing to a course they own, while the actual content (lesson, quiz, assignment) belonged to a different instructor’s course. The permission check would pass because it evaluated against the attacker-controlled course, not the one that actually owned the target content.nnThe attack vector is straightforward. An authenticated user with instructor-level access crafts a request to delete or modify content (e.g., a lesson) using the standard WordPress AJAX or admin handlers. The request must include a `course` GET parameter set to one of the attacker’s own course IDs, while the content ID (e.g., `lesson_id`, `topic_id`) targets a victim’s course content. The `get_course_id_by()` function returns the attacker’s course ID, and `can_user_manage()` confirms the attacker manages that course. The plugin then performs the requested operation on the victim’s content without further checks. For lesson deletion, the request might be a GET to `/wp-admin/admin.php?page=tutor&action=delete&lesson_id=VICTIM_LESSON_ID&course=ATTACKER_COURSE_ID`.nnThe patch removes two lines from the `get_course_id_by()` function (lines 7829-7830 in the old code). The vulnerable code block that checked for and immediately returned the `course` GET parameter is entirely deleted. After the patch, `get_course_id_by()` always performs the proper database lookup to find the actual course that owns the specified content object (lesson, quiz, etc.). It no longer trusts user-supplied input for ownership mapping. This ensures that `can_user_manage()` receives the correct course ID for authorization.nnSuccessful exploitation enables an authenticated instructor to arbitrarily delete lessons, assignments, quizzes (with cascading deletion of all student attempt data), topics, announcements, and Q&A threads across any course in the system. The attacker can also create or modify lessons, topics, and announcements in victim courses, manipulate student quiz grades, and read unpublished lesson and quiz content. This represents a complete breach of content isolation within the LMS, with potential for widespread data loss.”,
“poc_php”: null,
modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-6965n# This rule blocks IDOR exploitation by preventing the ‘course’ GET parameter from being usedn# in contexts where it is not the primary object being accessed.n# The vulnerability requires the ‘course’ parameter to differ from the actual content owner.n# This rule blocks any request that includes both a ‘course’ GET parameter and anothern# content ID parameter (lesson_id, topic_id, quiz_id, assignment_id) where the coursen# does not match the legitimate owner.n# However, since the legitimate course_id is unknown at the WAF layer, we block then# parameter combination entirely for instructor-level operations.nSecRule REQUEST_URI “@streq /wp-admin/admin.php” \n “id:20266965,phase:2,deny,status:403,chain,msg:’CVE-2026-6965 IDOR via course parameter manipulation’,severity:’CRITICAL’,tag:’CVE-2026-6965′,tag:’WordPress’,tag:’Tutor LMS'”n SecRule QUERY_STRING “@contains page=tutor” “chain”n SecRule QUERY_STRING “@rx (?:lesson_id|topic_id|quiz_id|assignment_id|announcement_id|qa_id)=d+” “chain”n SecRule QUERY_STRING “@rx (?:&|?)course=d+”
}

CVE-2026-6965: Tutor LMS <= 3.9.9 – Insecure Direct Object Reference to Authenticated (Instructor+) Arbitrary Post Deletion via 'course' GET Parameter (tutor)
CVE-2026-6965
tutor
3.9.9
3.9.10
Analysis Overview
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/tutor/classes/Admin.php
+++ b/tutor/classes/Admin.php
@@ -37,7 +37,6 @@
public function __construct() {
add_action( 'admin_notices', array( $this, 'show_unstable_version_admin_notice' ) );
- add_action( 'admin_notices', array( $this, 'show_v4_beta_notice' ) );
add_action( 'admin_menu', array( $this, 'register_menu' ) );
// Force activate menu for necessary.
@@ -87,61 +86,6 @@
}
/**
- * Show version 4 admin notice.
- *
- * @since 3.9.9
- *
- * @return void
- */
- public function show_v4_beta_notice() {
- if ( version_compare( TUTOR_VERSION, '4', '<' ) ) {
- ?>
- <div class="tutor-v4-beta-notice notice is-dismissible">
- <div class="tutor-v4-beta-notice-left">
- <img src="<?php echo esc_url( tutor()->url . 'assets/images/v4-notice-logo.svg' ); ?>" alt="Tutor LMS 4.0 Beta">
- </div>
- <div class="tutor-v4-beta-notice-right">
- <div class="tutor-v4-beta-notice-right-content">
- <h3><?php esc_html_e( 'Be the First to Try Tutor LMS 4.0 Beta!', 'tutor' ); ?></h3>
- <p>
- <?php
- echo wp_kses(
- sprintf(
- /* translators: 1: opening anchor tag, 2: closing anchor tag */
- __(
- 'Explore the upcoming features of Tutor LMS 4.0, test the experience, and help us improve with your valuable %1$sfeedback%2$s.',
- 'tutor'
- ),
- '<a href="https://forms.gle/Dxc1CWT63UcEAJGR9" target="_blank" rel="noopener noreferrer">',
- ' <i class="tutor-icon-external-link" aria-hidden="true"></i></a>'
- ),
- array(
- 'a' => array(
- 'href' => true,
- 'target' => true,
- 'rel' => true,
- ),
- 'i' => array(
- 'class' => true,
- 'aria-hidden' => true,
- ),
- )
- );
- ?>
- </p>
- </div>
- <div class="tutor-v4-beta-notice-right-buttons">
- <a href="https://tutorlms.com/blog/first-look-into-tutor-lms-4-0/?nocache=1" target="_blank" rel="noopener noreferrer" class="tutor-btn tutor-btn-tertiary tutor-gap-4px tutor-text-nowrap">
- <?php esc_html_e( 'Try now', 'tutor' ); ?>
- </a>
- </div>
- </div>
- </div>
- <?php
- }
- }
-
- /**
* Register admin menus
*
* @since 1.0.0
@@ -289,14 +233,6 @@
'zoom' => null,
'google_meet' => null,
'h5p' => null,
- 'themes' => array(
- 'parent_slug' => 'tutor',
- 'page_title' => __( 'Themes', 'tutor' ),
- 'menu_title' => __( 'Themes', 'tutor' ),
- 'capability' => 'manage_tutor',
- 'menu_slug' => 'tutor-themes',
- 'callback' => array( $this, 'tutor_themes' ),
- ),
'addons' => array(
'parent_slug' => 'tutor',
'page_title' => __( 'Addons', 'tutor' ),
@@ -369,15 +305,6 @@
}
/**
- * Tutor template view
- *
- * @since 3.6.0
- */
- public function tutor_themes() {
- include tutor()->path . 'views/template-import/templates.php';
- }
-
- /**
* Welcome page opt-out
*
* @since 3.0.0
--- a/tutor/classes/Assets.php
+++ b/tutor/classes/Assets.php
@@ -246,11 +246,6 @@
if ( 'tutor-addons' === $page ) {
wp_enqueue_script( 'tutor-coupon', tutor()->url . 'assets/js/tutor-addon-list.js', array( 'wp-i18n', 'wp-element' ), TUTOR_VERSION, true );
}
-
- if ( 'tutor-themes' === $page ) {
- wp_enqueue_style( 'tutor-template-import', tutor()->url . 'assets/css/tutor-template-import.min.css', array(), TUTOR_VERSION, 'all' );
- wp_enqueue_script( 'tutor-template-import-js', tutor()->url . 'assets/js/tutor-template-import-script.js', array( 'wp-i18n' ), TUTOR_VERSION, true );
- }
}
/**
--- a/tutor/classes/Tutor.php
+++ b/tutor/classes/Tutor.php
@@ -14,7 +14,6 @@
use TutorEcommerceEcommerce;
use TutorHelpersQueryHelper;
use TutorMigrationsMigration;
-use TutorTemplateImportTemplateImportInit;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -510,9 +509,6 @@
$this->course_filter = new Course_Filter();
$this->permalink = new Permalink();
- // Template import.
- new TemplateImportInit();
-
// Integrations.
$this->woocommerce = new WooCommerce();
$this->edd = new TutorEDD();
@@ -634,11 +630,22 @@
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
- $is_droip_active = is_plugin_active( 'droip/droip.php' );
- $tutor_droip_path = $tutor_path . 'includes/droip/droip.php';
- if ( $is_droip_active && file_exists( $tutor_droip_path ) ) {
- include $tutor_droip_path;
+ // Only kirki latest has class KirkiMain.
+ $is_kirki_active = is_plugin_active( 'kirki-pro/kirki-pro.php' ) && class_exists( 'KirkiProMain' );
+
+ if ( $is_kirki_active ) {
+ $tutor_kirki_path = $tutor_path . 'includes/kirki/kirki.php';
+ if ( file_exists( $tutor_kirki_path ) ) {
+ include $tutor_kirki_path;
+ }
+ } else {
+ $is_droip_active = is_plugin_active( 'droip/droip.php' );
+ $tutor_droip_path = $tutor_path . 'includes/droip/droip.php';
+ if ( $is_droip_active && file_exists( $tutor_droip_path ) ) {
+ include $tutor_droip_path;
+ }
}
+
}
/**
--- a/tutor/classes/Utils.php
+++ b/tutor/classes/Utils.php
@@ -7826,11 +7826,6 @@
* @return int|int[]
*/
public function get_course_id_by( $content, $object_id ) {
- $course_id = Input::get( 'course', 0, Input::TYPE_INT );
- if ( $course_id ) {
- return $course_id;
- }
-
$cache_key = "tutor_get_course_id_by_{$content}_{$object_id}";
$course_id = TutorCache::get( $cache_key );
--- a/tutor/includes/droip/backend/ElementGenerator/ActionsGenerator.php
+++ b/tutor/includes/droip/backend/ElementGenerator/ActionsGenerator.php
@@ -137,17 +137,19 @@
}
case 'start_learning_btn': {
- if (! $entry_box_button_logic->show_start_learning_btn) {
- return '';
- }
- $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
- if ($is_course_completed) {
- return '';
- }
- $lession_url = tutor_utils()->get_course_first_lesson($course_id);
- $extra_attributes .= " data-lession_url=$lession_url";
- return $this->generate_child_element_with_parent_droip_data($extra_attributes);
+ if (!$entry_box_button_logic->show_start_learning_btn) {
+ return '';
}
+ $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
+ if ($is_course_completed) {
+ return '';
+ }
+ $lession_url = tutor_utils()->get_course_first_lesson($course_id);
+
+ $is_public_course = get_post_meta($course_id, '_tutor_is_public_course', true);
+ $extra_attributes .= " data-lession_url=$lession_url data-course_is_public=$is_public_course";
+ return $this->generate_child_element_with_parent_droip_data($extra_attributes);
+ }
case 'continue_learning_btn': {
if (! $entry_box_button_logic->show_continue_learning_btn) {
--- a/tutor/includes/kirki/backend/Ajax.php
+++ b/tutor/includes/kirki/backend/Ajax.php
@@ -0,0 +1,191 @@
+<?php
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirki;
+
+use KirkiHelperFunctions;
+use stdClass;
+use TutorLMSKirkiElementGeneratorPreview;
+use TUTORInput;
+use TutorModelsCourseModel;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Ajax
+ * This class is used to define all ajax functions.
+ *
+ * @package TutorLMSKirki
+ * @since 1.0.0
+ */
+class Ajax {
+
+
+ /**
+ * Class constructor
+ */
+ public function __construct() {
+ add_action( 'wp_ajax_tutor_handle_api_calls', array( $this, 'tutor_handle_api_calls' ) );
+ add_action( 'wp_ajax_nopriv_tutor_handle_api_calls', array( $this, 'tutor_handle_api_calls' ) );
+ }
+
+ /**
+ * Handle api calls
+ *
+ * @since 1.0.0
+ */
+ public function tutor_handle_api_calls() {
+ $request_method = Input::post( 'method' );
+ tutor_utils()->checking_nonce();
+ if ( 'generate_html' === $request_method ) {
+
+ $course_id = Input::post( 'course_id' );
+ $kirki_data = isset( $_REQUEST['kirki_data'] ) ? wp_unslash( $_REQUEST['kirki_data'] ) : null; //phpcs:ignore
+ $kirki_data = json_decode( $kirki_data, true );
+
+ $blocks = $kirki_data['blocks'];
+ $styles = $kirki_data['styles'];
+ $root = Input::sanitize( $kirki_data['root'] );
+
+ $params = array(
+ 'blocks' => $blocks,
+ 'style_blocks' => $styles,
+ 'root' => $root,
+ 'get_variable' => false,
+ 'get_fonts' => false,
+ 'options' => array( 'post' => get_post( $course_id ) ),
+ );
+
+ $collection_wrapper_html_string = HelperFunctions::get_html_using_preview_script( $params );
+ wp_send_json_success( $collection_wrapper_html_string );
+ }
+ if ( 'enroll_course' === $request_method ) {
+ $course_id = Input::post( 'course_id' );
+ $res = tutor_utils()->do_enroll( $course_id );
+ wp_send_json_success( $res );
+ }
+ if ( 'add_to_cart_course' === $request_method ) {
+ $course_id = Input::post( 'course_id' );
+ $res = tutor_add_to_cart( $course_id );
+
+ // check is user logged in or not
+ if (! is_user_logged_in() ) {
+ $res['redirect'] = true;
+ $res['data'] = wp_login_url( wp_get_referer() );
+ }
+
+ wp_send_json_success( $res );
+ }
+
+ if ('remove_from_cart_course' === $request_method) {
+ $res = tutor_remove_cart_item(Input::post('course_id'));
+ wp_send_json_success($res);
+ }
+
+ if('get_user_cart_item_count' === $request_method) {
+ $cart_items = tutor_get_cart_items();
+ $count = count($cart_items);
+ wp_send_json_success($count);
+ }
+
+ if ( 'complete_course' === $request_method ) {
+ $course_id = Input::post( 'course_id' );
+ $user_id = get_current_user_id();
+ if ( ! $user_id ) {
+ wp_send_json_error( 'Please Sign-In' );
+ }
+ CourseModel::mark_course_as_completed( $course_id, $user_id );
+
+ wp_send_json_success( true );
+ }
+
+ if ( 'add_qna' === $request_method ) {
+ $course_id = Input::post( 'course_id' );
+ $comment_parent_id = Input::post( 'comment_parent_id' );
+ $content = Input::post( 'content' );
+
+ $user = wp_get_current_user();
+ $date = gmdate( 'Y-m-d H:i:s', tutor_time() );
+
+ if ( ! $user->ID ) {
+ wp_send_json_error( 'Please Sign-In' );
+ }
+
+ $collection_data = isset($_REQUEST['collection_data']) ? json_decode(wp_unslash($_REQUEST['collection_data']), true) : null; //phpcs:ignore
+
+ if ( ! $content ) {
+ wp_send_json_error( 'Invalid request' );
+ }
+
+ $data = apply_filters(
+ 'tutor_qna_insert_data',
+ array(
+ 'comment_post_ID' => $course_id,
+ 'comment_author' => $user->user_login,
+ 'comment_date' => $date,
+ 'comment_date_gmt' => get_gmt_from_date( $date ),
+ 'comment_content' => $content,
+ 'comment_approved' => 'approved',
+ 'comment_agent' => 'TutorLMSPlugin',
+ 'comment_type' => 'tutor_q_and_a',
+ 'comment_parent' => $comment_parent_id,
+ 'user_id' => $user->ID,
+ )
+ );
+
+ global $wpdb;
+
+ $response = $wpdb->insert( $wpdb->comments, $data );
+
+ if ( false === $response ) {
+ wp_send_json_error( 'Request failed!' );
+ }
+
+ $thread = $this->get_comment( $wpdb->insert_id );
+
+ // comment-item.// -qna-reply.
+ $new_element_name = 0 === $comment_parent_id ? 'comment-item' : TDE_APP_PREFIX . '-qna-reply';
+
+ $new_element = Preview::generateQnAElement( $thread, $new_element_name, $collection_data );
+
+ wp_send_json_success(
+ array(
+ 'html' => $new_element,
+ 'inserted_comment_id' => $wpdb->insert_id,
+ )
+ );
+ }
+
+ wp_send_json_error( 'Invalid request' );
+ }
+
+ /**
+ * Get comment
+ *
+ * @param int $id comment id.
+ * @return object
+ * @since 1.0.0
+ */
+ private function get_comment( $id ) {
+ $comment = (object) (array) get_comment( $id );
+
+ if ( $comment instanceof stdClass ) {
+ $author_posts_page_link = $comment->comment_author_url;
+
+ if ( ! $author_posts_page_link ) {
+ $author_posts_page_link = get_author_posts_url( $comment->user_id );
+ }
+
+ $comment->author_profile_picture = get_avatar_url( $comment->user_id );
+ $comment->author_posts_page_link = $author_posts_page_link;
+ }
+
+ return $comment;
+ }
+}
--- a/tutor/includes/kirki/backend/Backend.php
+++ b/tutor/includes/kirki/backend/Backend.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirki;
+
+use TutorLMSKirkiElementGeneratorElementGenerator;
+use TUTORInput;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Backend
+ *
+ * @package TutorLMSKirki
+ */
+class Backend {
+
+
+ /**
+ * Backend constructor.
+ */
+ public function __construct() {
+ $this->run();
+ }
+
+ /**
+ * Run the backend
+ */
+ public function run() {
+ $action = Input::get( 'action' );
+ if ( 'kirki' === $action ) {
+ $load_for = Input::get( 'load_for' );
+ if ( 'kirki-iframe' === $load_for ) {
+ new Iframe();
+ } else {
+ new Editor();
+ }
+ }
+ new ElementGenerator();
+ new Pages();
+ new Hooks();
+ }
+}
--- a/tutor/includes/kirki/backend/Editor.php
+++ b/tutor/includes/kirki/backend/Editor.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirki;
+
+if (! defined('ABSPATH')) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Editor
+ * This class is used to define all editor functions.
+ */
+class Editor
+{
+
+ /**
+ * Class constructor
+ *
+ * @since 1.0.0
+ */
+ public function __construct()
+ {
+ add_action('wp_enqueue_scripts', [$this, 'load_assets'], 100);
+ }
+
+ /**
+ * Load assets
+ *
+ * @since 1.0.0
+ */
+ public function load_assets()
+ {
+ wp_enqueue_script(TDE_APP_PREFIX . '-tutor-kirki-elements', TDE_PLUGIN_ROOT_BASE . 'build/js/editor.min.js', ['wp-i18n', 'kirki-editor'], TDE_APP_VERSION, true);
+ wp_enqueue_style(TDE_APP_PREFIX . '-tutor-kirki-elements', TDE_PLUGIN_ROOT_BASE . 'build/css/editor.min.css', null, TDE_APP_VERSION);
+ }
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/ActionsGenerator.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/ActionsGenerator.php
@@ -0,0 +1,364 @@
+<?php
+
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+use TUTORCourse;
+use TUTOR_CERTCertificate;
+use TutorProSubscriptionSubscription;
+use TutorProSubscriptionModelsPlanModel;
+use TutorProSubscriptionModelsSubscriptionModel;
+use TutorEcommerceCheckoutController;
+use TutorProGiftCourseGiftCourse;
+use TutorProSubscriptionSettings;
+
+if (! defined('ABSPATH')) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class ActionsGenerator
+ * This class is used to define all helper functions.
+ *
+ * @package TutorLMSKirkiElementGenerator
+ */
+trait ActionsGenerator
+{
+
+ /**
+ * Generate actionbox markup
+ *
+ * @return string
+ */
+ private function generate_action_markup()
+ {
+ $course_id = isset($this->options['post']) ? $this->options['post']->ID : get_the_ID();
+ $ele_name = $this->element['name'];
+ $entry_box_button_logic = tutor_entry_box_buttons($course_id);
+ $type = isset($this->properties['type']) ? $this->properties['type'] : 'enroll_btn';
+ $extra_attributes = "data-course_id='$course_id' data-action_type='$type'";
+
+ $selling_option = Course::get_selling_option($course_id);
+ if (!$selling_option) {
+ $selling_option = Course::SELLING_OPTION_ALL;
+ }
+
+ switch ($type) {
+ case 'wishlist_btn': {
+ if (! is_user_logged_in()) {
+ return '';
+ }
+ $is_wish_listed = tutor_utils()->is_wishlisted($course_id, get_current_user_id());
+ if ($is_wish_listed) {
+ return '';
+ }
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+ case 'wishlisted_btn': {
+ if (! is_user_logged_in()) {
+ return '';
+ }
+ $is_wish_listed = tutor_utils()->is_wishlisted($course_id, get_current_user_id());
+ if (! $is_wish_listed) {
+ return '';
+ }
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+ }
+
+ $entry_box_button_logic = $this->update_entry_box_button_logic($entry_box_button_logic, $this->options);
+
+ if (! isset($entry_box_button_logic->{'show_' . $type}) || (isset($entry_box_button_logic->{'show_' . $type}) && $entry_box_button_logic->{'show_' . $type} !== true)) {
+ return '';
+ }
+ switch ($type) {
+ case 'enroll_btn': {
+ if (! $entry_box_button_logic->show_enroll_btn) {
+ return '';
+ }
+
+ if (tutor()->has_pro && Subscription::is_enabled()) {
+ $subscription_model = new SubscriptionModel();
+ $tutor_subscription_enrollment = false;
+
+ // For hybrid mode.
+ if (Course::PRICE_TYPE_PAID === tutor_utils()->price_type($course_id) && $subscription_model->has_course_access($course_id)) {
+ $tutor_subscription_enrollment = true;
+ }
+
+ // For membership only mode.
+ if (Settings::membership_only_mode_enabled() && $subscription_model->has_course_access($course_id)) {
+ $tutor_subscription_enrollment = true;
+ }
+
+ if ($tutor_subscription_enrollment) {
+ $extra_attributes .= " data-tutor_subscription_enrollment='true'";
+ }
+ }
+
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'add_to_cart_btn': {
+ $is_course_in_user_cart = tutor_is_item_in_cart($course_id);
+ if ($is_course_in_user_cart) {
+ return '';
+ }
+
+ if ($selling_option === Course::SELLING_OPTION_ALL || $selling_option === Course::SELLING_OPTION_ONE_TIME || $selling_option === Course::SELLING_OPTION_BOTH) {
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+ return "";
+ }
+
+ case 'remove_from_cart_btn': {
+ $is_course_in_user_cart = tutor_is_item_in_cart($course_id);
+ if (!$is_course_in_user_cart) {
+ return '';
+ }
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'view_cart_btn': {
+ $is_course_in_user_cart = tutor_is_item_in_cart($course_id);
+ if (! $is_course_in_user_cart) {
+ return '';
+ }
+ $extra_attributes .= " data-cart_url='" . tutor_get_cart_url() . "'";
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'start_learning_btn': {
+ if (! $entry_box_button_logic->show_start_learning_btn) {
+ return '';
+ }
+ $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
+ if ($is_course_completed) {
+ return '';
+ }
+ $lession_url = tutor_utils()->get_course_first_lesson($course_id);
+
+ $is_public_course = get_post_meta($course_id, '_tutor_is_public_course', true);
+ $extra_attributes .= " data-lession_url=$lession_url data-course_is_public=$is_public_course";
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'continue_learning_btn': {
+ if (! $entry_box_button_logic->show_continue_learning_btn) {
+ return '';
+ }
+ $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
+ if ($is_course_completed) {
+ return '';
+ }
+ $extra_attributes .= " data-continue_learning_url='" . tutor_utils()->get_course_first_lesson() . "'";
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'complete_course_btn': {
+ if (! $entry_box_button_logic->show_complete_course_btn) {
+ return '';
+ }
+ $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
+ if ($is_course_completed) {
+ return '';
+ }
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'retake_course_btn': {
+ if ($entry_box_button_logic->show_retake_course_btn || ($entry_box_button_logic->show_certificate_view_btn && function_exists('TUTOR_CERT'))) {
+ $extra_attributes .= " data-continue_learning_url='" . tutor_utils()->get_course_first_lesson() . "'";
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ return "";
+ }
+ case 'certificate_view_btn': {
+ if (! function_exists('TUTOR_CERT')) {
+ return '';
+ }
+ if (! $entry_box_button_logic->show_certificate_view_btn) {
+ return '';
+ }
+ $is_course_completed = tutor_utils()->is_completed_course($course_id, get_current_user_id());
+
+ if (! $is_course_completed) {
+ return '';
+ }
+ if (! $course_id) {
+ return '';
+ }
+
+ if (tutils()->is_addon_enabled(TUTOR_CERT()->basename)) {
+ $has_course_certificate_template = (new Certificate(true))->has_course_certificate_template($course_id);
+ if (!$has_course_certificate_template) {
+ return "";
+ }
+
+ $certificate_url = '';
+ $certificate_url = (new Certificate(true))->get_certificate($course_id);
+
+ $extra_attributes .= " data-certificate_url='" . $certificate_url . "'";
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ return '';
+ }
+
+ case 'subscribe_now_btn': {
+ $checkout_link = CheckoutController::get_page_url();
+
+
+ if (is_user_logged_in()) {
+ $extra_attributes .= " data-checkout_url='" . $checkout_link . "'";
+ } else {
+ $login_url = wp_login_url(wp_get_referer());
+
+ $extra_attributes .= " data-login_url='" . $login_url . "'";
+ }
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ case 'membership_btn': {
+ $pricing_page = Settings::get_pricing_page_url();
+ if ($pricing_page) {
+ $extra_attributes .= " data-pricing_url='" . $pricing_page . "'";
+ // if (is_user_logged_in()) { // removed to always direct to pricing page
+ // } else {
+ // $login_url = wp_login_url(wp_get_referer());
+
+ // $extra_attributes .= " data-login_url='" . $login_url . "'";
+ // }
+
+ return $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ }
+
+ return "";
+ }
+
+ case 'gift_course_btn': {
+ if (is_user_logged_in()) {
+ $modal_id = 'tutor-gift-this-course-modal-' . wp_generate_uuid4();
+ $extra_attributes .= " data-tutor-modal-target='$modal_id'";
+
+ $btn = $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+ // Capture template output
+ ob_start();
+ tutor_load_template(
+ 'single.course.gift-this-course-modal',
+ array('course_id' => $course_id, 'modal_id' => $modal_id),
+ true
+ );
+
+ $modal_html = ob_get_clean();
+
+ // Return button + modal
+ return $btn . $modal_html;
+ } else {
+ // direct to login page if not logged in
+ $login_url = wp_login_url(wp_get_referer());
+ $extra_attributes .= " data-login_url='" . $login_url . "'";
+ $btn = $this->generate_child_element_with_parent_kirki_data($extra_attributes);
+
+ // Return button + modal
+ return $btn;
+ }
+
+ return "";
+ }
+
+ default: {
+ return '';
+ }
+ }
+
+ return '';
+ }
+
+ private function update_entry_box_button_logic($entry_box_button_logic, $options)
+ {
+ $course_id = isset($options['post']) ? $options['post']->ID : get_the_ID();
+
+ $is_paid_course = tutor_utils()->is_course_purchasable($course_id);
+
+ if (isset($options['relation_type']) && $options['relation_type'] === 'TUTOR_LMS_CART') {
+ if ($entry_box_button_logic->show_view_cart_btn) {
+ $entry_box_button_logic->show_remove_from_cart_btn = true;
+ }
+ }
+ if (isset($options['relation_type']) && $options['relation_type'] === 'TUTOR_LMS_CART') {
+ $entry_box_button_logic->show_view_cart_btn = false;
+ }
+
+ // Remove this part. These logic come from tutor_entry_box_buttons function.
+ // if ($is_paid_course) {
+
+ // if (tutor()->has_pro && Subscription::is_enabled() && $course_id) {
+
+ // $plan_model = new PlanModel();
+ // // Checking is course has subscription plan then show buy now button.
+ // $selling_option = Course::get_selling_option($course_id);
+ // if (!$selling_option) {
+ // $selling_option = Course::SELLING_OPTION_ALL;
+ // }
+ // if ($selling_option === Course::SELLING_OPTION_SUBSCRIPTION || $selling_option === Course::SELLING_OPTION_BOTH || $selling_option === Course::SELLING_OPTION_ALL) {
+
+ // $items = $plan_model->get_subscription_plans($course_id, PlanModel::STATUS_ACTIVE);
+
+ // if (count($items) > 0) {
+ // $entry_box_button_logic->show_subscribe_now_btn = true;
+ // }
+ // }
+
+ // // Checking is course has membership plan enabled
+ // $selling_option = Course::get_selling_option($course_id);
+ // if (!$selling_option) {
+ // $selling_option = Course::SELLING_OPTION_ALL;
+ // }
+ // if ($selling_option === Course::SELLING_OPTION_MEMBERSHIP || $selling_option === Course::SELLING_OPTION_ALL) {
+ // $active_membership_plans = $plan_model->get_membership_plans(PlanModel::STATUS_ACTIVE);
+ // if (count($active_membership_plans) > 0) {
+ // $entry_box_button_logic->show_membership_btn = true;
+ // }
+ // }
+ // }
+
+ // // Checking is course can be gifted then show gift course button.
+ // if (class_exists('TutorProGiftCourseInitGift') && class_exists('TutorProGiftCourseGiftCourse')) {
+ // $init_gift = new TutorProGiftCourseInitGift();
+ // if (tutor()->has_pro && $init_gift->is_enabled() && $course_id) {
+ // $can_gift_this_course = GiftCourse::can_gift_course($course_id);
+
+ // if ($can_gift_this_course) {
+ // $entry_box_button_logic->show_gift_course_btn = true;
+ // }
+ // }
+ // }
+ // }
+
+ return $entry_box_button_logic;
+ }
+
+ private function generate_child_element_with_parent_kirki_data($extra_attributes)
+ {
+ $children_html = $this->generate_child_elements();
+ // echo "<pre>";var_dump($this->element['parentId'], $this->elements[$this->element['parentId'] ]);die;
+ if (isset($this->elements[$this->element['parentId']])) {
+ $encoded_data = $this->get_all_data_and_styles_from_element_id($this->element['parentId']);
+ $encoded_data = json_encode($encoded_data);
+ $children_html .= "<textarea style='display: none'>$encoded_data</textarea>";
+ }
+ return $this->generate_common_element(false, $children_html, $extra_attributes);
+ }
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/AddRatingGenerator.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/AddRatingGenerator.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Rating genrator
+ *
+ * @package TutorLMSKirkiElementGenerator
+ */
+trait AddRatingGenerator {
+
+ /**
+ * Generate Rating elements
+ *
+ * @return string
+ */
+ private function generate_add_rating_element() {
+ $ele_name = $this->element['name'];
+ $rating = isset( $this->options['rating'] ) ? $this->options['rating'] : 0;
+ switch ( $ele_name ) {
+ case TDE_APP_PREFIX . '-add-rating':
+ $children_html = $this->generate_child_elements();
+ $children_html .= '<input type="hidden" name="rating" value="' . $rating . '">';
+ $html = $this->generate_common_element( false, $children_html );
+ return $html;
+ case TDE_APP_PREFIX . '-active-stars':
+ $children_html = '';
+ for ( $i = 0; $i < $rating; $i++ ) {
+ $children_html .= $this->generate_common_element( false, false, 'data-star_index="' . $i . '"' );
+ }
+ for ( $i = 0; $i < 5 - $rating; $i++ ) {
+ $children_html .= $this->generate_common_element( true );
+ }
+ return $children_html;
+ case TDE_APP_PREFIX . '-inactive-stars':
+ $children_html = '';
+ for ( $i = 0; $i < 5 - $rating; $i++ ) {
+ $children_html .= $this->generate_common_element( false, false, 'data-star_index="' . ( $rating + $i ) . '"' );
+ }
+ for ( $i = 0; $i < $rating; $i++ ) {
+ $children_html .= $this->generate_common_element( true );
+ }
+ return $children_html;
+ }
+ }
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/CourseMetaGenerator.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/CourseMetaGenerator.php
@@ -0,0 +1,824 @@
+<?php
+
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+use KirkiHelperFunctions;
+use TutorModelsCourseModel;
+use TutorEcommerceCartController;
+use TutorEcommerceTax;
+use TutorModelsOrderModel;
+
+if (! defined('ABSPATH')) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class CourseMetaGenerator
+ * This class is used to define all helper functions.
+ */
+trait CourseMetaGenerator
+{
+
+ /**
+ * Generate course meta markup
+ *
+ * @return string
+ */
+ private function generate_course_meta_markup()
+ {
+ self::fill_dynamic_data_if_symbol_html_generation();
+ $settings = isset($this->element['properties']['settings']) ? $this->element['properties']['settings'] : array();
+ $meta_type = isset($settings['course_meta_type']) ? $settings['course_meta_type'] : 'default';
+ $course_id = isset($this->options['post']) ? $this->options['post']->ID : get_the_ID();
+ $is_instructor = isset($this->options['itemType']) && ($this->options['itemType'] !== 'post' || $this->options['itemType'] === 'user');
+ if (isset($this->options['itemType']) && $this->options['itemType'] === 'user') $course_id = $this->options['user']['ID'];
+ $meta = $this->get_course_meta($meta_type, $course_id, $this->options, $settings, $is_instructor);
+ $meta = $this->wrap_if_meta_has_label($meta, $settings);
+
+ if ($meta_type === 'sidebar_meta') {
+ $html = '';
+ foreach ($meta as $key => $single_meta) {
+ if (! $single_meta['value']) {
+ continue;
+ }
+ $options['sidebar_meta'] = $single_meta;
+ if (! isset($options['sidebar_meta'])) {
+ return '';
+ }
+ $sidebar_meta = $options['sidebar_meta'];
+ $value = $sidebar_meta['value'];
+ $html .= "<span $this->attributes>$value</span>";
+ }
+ return "<div $this->attributes>$html</div>";
+ } elseif ($meta_type === 'stars' || $meta_type === 'instructor_stars') {
+ $this->options['star_type'] = $meta_type;
+ return "<div $this->attributes>" . $this->generate_child_elements() . '</div>';
+ } elseif ($meta_type === 'active_stars' || $meta_type === 'inactive_stars' || $meta_type === 'instructor_active_stars' || $meta_type === 'instructor_inactive_stars') {
+ $html = '';
+ for ($i = 1; $i <= round((int) $meta); $i++) {
+ $html .= $this->generate_common_element();
+ }
+ $this->generate_common_element();
+ return $html;
+ } elseif (str_contains($meta_type, '_star_progress_percentage') || $meta_type === 'course_progress_bar') {
+ return "<div $this->attributes style='height:100%;width:$meta%;'>" . $this->generate_child_elements() . '</div>';
+ } elseif (
+ $meta_type === 'course_benefits' ||
+ $meta_type === 'course_materials' ||
+ $meta_type === 'course_requirements' ||
+ $meta_type === 'course_target_audience'
+ ) {
+ $html = '';
+ foreach ($meta as $single_meta) {
+ $this->options['course_meta_child_content'] = $single_meta;
+ $html .= $this->generate_child_elements();
+ }
+ return "<div $this->attributes>" . $html . '</div>';
+ } elseif (
+ $meta_type === 'single_benefit' ||
+ $meta_type === 'single_material' ||
+ $meta_type === 'single_requirement' ||
+ $meta_type === 'single_target_audience' ||
+ $meta_type === 'single_numeric_meta'
+ ) {
+ $content = $this->options['course_meta_child_content'];
+ unset($this->options['course_meta_child_content']);
+ return "<span $this->attributes>" . $content . '</span>';
+ } elseif ($meta_type === 'instructor_image' || $meta_type === 'comment_author_image') {
+ return "<img $this->attributes src='" . $meta['src'] . "' alt='" . $meta['alt'] . "'/>";
+ } else {
+ return "<span $this->attributes>$meta</span>";
+ }
+ }
+
+
+ private function fill_dynamic_data_if_symbol_html_generation()
+ {
+ if (isset($this->options['comment']) && isset($this->options['comment']->collectionType)) {
+ $comment_id = ((array) $this->options['comment'])['comment_ID'];
+ $reviews = tutor_utils()->get_course_reviews($comment_id, 0, 100, false, array('approved'), get_current_user_id(), false);
+ $review = count($reviews) > 0 ? $reviews[0] : array();
+ $qna_data = tutor_utils()->get_qa_question($comment_id);
+ $qna = $qna_data ? $qna_data : array();
+ $comment = array_merge((array) $review, (array) $qna);
+ $comment['author_profile_picture'] = array('src' => get_avatar_url($comment['user_id']));
+ $this->options['comment'] = $comment;
+ }
+ if (isset($this->options['announcement']) && isset($this->options['announcement']->collectionType)) {
+ $announcement_id = ((array) $this->options['announcement'])['ID'];
+ $this->options['announcement'] = get_post($announcement_id);
+ }
+ if (isset($this->options['resources']) && isset($this->options['resources']->collectionType)) {
+ $resource_id = ((array) $this->options['resources'])['id'];
+ $resource = tutor_utils()->get_attachment_data($resource_id);
+ $this->options['resources'] = $resource;
+ }
+ }
+
+ public function wrap_if_meta_has_label($meta, $settings)
+ {
+ $label = isset($settings['label']) ? $settings['label'] : false;
+ if ($label) {
+ $singular = isset($settings['singular']) ? $settings['singular'] : '';
+ $plural = isset($settings['plural']) ? $settings['plural'] : '';
+ $suffix = $meta > 1 ? $plural : $singular;
+ $meta = "{$meta} ";
+ $meta = $meta . $suffix;
+ }
+ return $meta;
+ }
+
+ public static function get_course_meta($meta_type, $course_id, $options = array(), $settings = array(), $is_instructor = false)
+ {
+ $current_user_id = get_current_user_id();
+ switch ($meta_type) {
+ case 'course_level':
+ $level_text = get_tutor_course_level($course_id);
+ return $level_text;
+ case 'enroll_count':
+ $count_value = tutor_utils()->count_enrolled_users_by_course($course_id);
+ return $count_value;
+ case 'course_duration':
+ $duration = get_post_meta($course_id, '_course_duration', true);
+ if (! $duration) {
+ return '';
+ }
+ $hour = (int) $duration['hours'] ?? '0';
+ $hour_text = $hour > 1 ? 'hours' : 'hour';
+ $minute = (int) $duration['minutes'] ?? '0';
+ $minute_text = $minute > 1 ? 'minutes' : 'minute';
+ return "{$hour} {$hour_text} {$minute} {$minute_text}";
+ case 'last_updated':
+ $date = get_the_modified_date(get_option('date_format'), $course_id);
+ $format = isset($settings['date_format']) ? $settings['date_format'] : 'M j, Y';
+ $date = self::format_date($date, $format);
+ return $date;
+ case 'course_price':
+ $course_price = tutor_utils()->get_raw_course_price($course_id);
+ $regular_price = $course_price->regular_price == 0 ? '' : $course_price->regular_price;
+ if ($regular_price == '') {
+ return '';
+ }
+ return tutor_get_formatted_price($regular_price);
+ case 'sale_price':
+ $course_price = tutor_utils()->get_raw_course_price($course_id);
+ $sale_price = $course_price->sale_price == 0 ? '' : $course_price->sale_price;
+ if ($sale_price == '') {
+ return '';
+ }
+ return tutor_get_formatted_price($sale_price);
+ case 'sidebar_meta':
+ $sidebar_metas = apply_filters('tutor/course/single/sidebar/metadata', array(), $course_id);
+ if (isset($sidebar_metas)) {
+ return $sidebar_metas;
+ }
+
+ return array();
+ case 'comment_author':
+ if (! isset($options['comment'])) {
+ return '';
+ }
+
+ $comments = json_decode(json_encode($options['comment']), true);
+ return $comments['display_name'] ? $comments['display_name'] : $comments['comment_author'];
+ case 'comment_author_image':
+ if (! isset($options['comment'])) {
+ return array(
+ 'src' => '',
+ 'alt' => ''
+ );
+ }
+ $comments = json_decode(json_encode($options['comment']), true);
+ $image_url = array(
+ 'src' => get_avatar_url($comments['user_id']),
+ 'alt' => $comments['display_name'],
+ );
+ return $image_url;
+ case 'comment_content':
+ case 'comment_date':
+ case 'rating':
+ if (! isset($options['comment'])) {
+ return '';
+ }
+ $comments = json_decode(json_encode($options['comment']), true);
+ if ($meta_type === 'comment_date') {
+ $format = isset($settings['date_format']) ? $settings['date_format'] : 'M j, Y';
+ $comments['comment_date'] = self::format_date($comments['comment_date'], $format, "Y-m-d H:i:s");
+ }
+ return $comments[$meta_type];
+ case 'active_stars':
+ case 'inactive_stars':
+ $comments = null;
+ if (isset($options['comment'])) {
+ $comments = json_decode(json_encode($options['comment']), true);
+ $stars = (int) $comments['rating'];
+ return $meta_type === 'active_stars' ? $stars : 5 - $stars;
+ }
+ $comments = self::get_course_reviews($course_id);
+ if (count($comments) === 0) {
+ return $meta_type === 'active_stars' ? 0 : 5;
+ }
+
+ $rating = 0;
+ foreach ($comments as $comment) {
+ $rating += $comment->rating;
+ }
+ $average = (int) round($rating / count($comments));
+ return $meta_type === 'active_stars' ? $average : 5 - $average;
+ case 'total_ratings':
+ $comments = self::get_course_reviews($course_id);
+ $count = count($comments);
+ return $count;
+ case 'average_rating':
+ $comments = self::get_course_reviews($course_id);
+ if (count($comments) == 0) {
+ return 0;
+ }
+
+ $rating = 0;
+ foreach ($comments as $comment) {
+ $rating += $comment->rating;
+ }
+ $average = number_format($rating / count($comments), 1);
+ return $average;
+ case 'rating_1_star_count':
+ $comments = self::get_course_reviews($course_id);
+ $rating_1_star_count = 0;
+ foreach ($comments as $comment) {
+ if ($comment->rating == 1) {
+ ++$rating_1_star_count;
+ }
+ }
+ return $rating_1_star_count;
+ case 'rating_2_star_count':
+ $comments = self::get_course_reviews($course_id);
+ $rating_2_star_count = 0;
+ foreach ($comments as $comment) {
+ if ($comment->rating == 2) {
+ ++$rating_2_star_count;
+ }
+ }
+ return $rating_2_star_count;
+ case 'rating_3_star_count':
+ $comments = self::get_course_reviews($course_id);
+ $rating_3_star_count = 0;
+ foreach ($comments as $comment) {
+ if ($comment->rating == 3) {
+ ++$rating_3_star_count;
+ }
+ }
+ return $rating_3_star_count;
+ case 'rating_4_star_count':
+ $comments = self::get_course_reviews($course_id);
+ $rating_4_star_count = 0;
+ foreach ($comments as $comment) {
+ if ($comment->rating == 4) {
+ ++$rating_4_star_count;
+ }
+ }
+ return $rating_4_star_count;
+ case 'rating_5_star_count':
+ $comments = self::get_course_reviews($course_id);
+ $rating_5_star_count = 0;
+ foreach ($comments as $comment) {
+ if ($comment->rating == 5) {
+ ++$rating_5_star_count;
+ }
+ }
+ return $rating_5_star_count;
+ case 'max_rating_count':
+ $comments = self::get_course_reviews($course_id);
+ $counts = array();
+ $counts[] = self::get_course_meta('rating_1_star_count', $course_id, $options, $settings);
+ $counts[] = self::get_course_meta('rating_2_star_count', $course_id, $options, $settings);
+ $counts[] = self::get_course_meta('rating_3_star_count', $course_id, $options, $settings);
+ $counts[] = self::get_course_meta('rating_4_star_count', $course_id, $options, $settings);
+ $counts[] = self::get_course_meta('rating_5_star_count', $course_id, $options, $settings);
+ $max_rating_count = 0;
+ foreach ($counts as $count) {
+ if ($count > $max_rating_count) {
+ $max_rating_count = $count;
+ }
+ }
+ return $max_rating_count;
+ case '1_star_progress_percentage':
+ case '2_star_progress_percentage':
+ case '3_star_progress_percentage':
+ case '4_star_progress_percentage':
+ case '5_star_progress_percentage':
+ case 'avg_star_progress_percentage':
+ $rating = str_replace('_star_progress_percentage', '', $meta_type);
+ if ($rating === 'avg') {
+ $this_count = self::get_course_meta('average_rating', $course_id, $options, $settings);
+ $max_count = 5;
+ return round(($this_count / $max_count) * 100);
+ }
+
+ $max_count = self::get_course_meta('max_rating_count', $course_id, $options, $settings);
+ if ($max_count == 0) {
+ return 0;
+ }
+
+ $this_count = self::get_course_meta('rating_' . $rating . '_star_count', $course_id, $options, $settings);
+ $percentage = round(($this_count / $max_count) * 100);
+ return $percentage;
+ case 'enroll_date':
+ $is_enrolled = tutor_utils()->is_enrolled($course_id);
+ $post_date = is_object($is_enrolled) && isset($is_enrolled->post_date) ? $is_enrolled->post_date : '';
+ $post_date = tutor_i18n_get_formated_date($post_date, get_option('date_format'));
+ $format = isset($settings['date_format']) ? $settings['date_format'] : 'M j, Y';
+ $post_date = self::format_date($post_date, $format);
+ return $post_date;
+ case 'progress_percent':
+ $is_enrolled = tutor_utils()->is_enrolled($course_id);
+ if (! $is_enrolled) {
+ return 0;
+ }
+ $course_progress = tutor_utils()->get_course_completed_percent($course_id, $current_user_id, true);
+ $completed_percent = (int) $course_progress['completed_percent'];
+ return $completed_percent;
+ case 'course_progress_bar':
+ return self::get_course_meta('progress_percent', $course_id, $options, $settings);
+ case 'completed_steps':
+ $is_enrolled = tutor_utils()->is_enrolled($course_id);
+ if (! $is_enrolled) {
+ return '';
+ }
+ $course_progress = tutor_utils()->get_course_completed_percent($course_id, $current_user_id, true);
+ return $course_progress['completed_count'];
+ case 'total_steps':
+ $is_enrolled = tutor_utils()->is_enrolled($course_id);
+ if (! $is_enrolled) {
+ return '';
+ }
+ $course_progress = tutor_utils()->get_course_completed_percent($course_id, $current_user_id, true);
+ return $course_progress['total_count'];
+
+ case 'lesson_duration':
+ $material = array();
+ if (isset($options['material'])) {
+ $material = (object) $options['material'];
+ } else {
+ $material = $options['post'];
+ }
+ $duration = self::get_material_duration($material);
+ return $duration;
+ case 'question_author':
+ $question = (object) $options['comment'];
+ if (! isset($question)) {
+ return '';
+ }
+
+ return $question->display_name ? $question->display_name : $question->comment_author;
+ case 'question_content':
+ $question = (object) $options['comment'];
+ if (! isset($question)) {
+ return '';
+ }
+
+ return $question->comment_content;
+ case 'question_date':
+ $question = (object) $options['comment'];
+ if (! isset($question)) {
+ return '';
+ }
+ $format = isset($settings['date_format']) ? $settings['date_format'] : 'M j, Y';
+ $date = self::format_date($question->comment_date, $format);
+ return $date;
+ case 'announcement_title':
+ $announcement = (object) $options['announcement'];
+ if (! isset($announcement)) {
+ return '';
+ }
+
+ return $announcement->post_title;
+ case 'announcement_content':
+ $announcement = (object) $options['announcement'];
+ if (! isset($announcement)) {
+ return '';
+ }
+
+ return $announcement->post_content;
+ case 'announcement_author':
+ $announcement = (object) $options['announcement'];
+ if (! isset($announcement)) {
+ return '';
+ }
+
+ $author_display_name = get_the_author_meta('display_name', $announcement->post_author);
+ return $author_display_name;
+ case 'announcement_date':
+ $announcement = (object) $options['announcement'];
+ if (! isset($announcement)) {
+ return '';
+ }
+ $format = isset($settings['date_format']) ? $settings['date_format'] : 'M j, Y';
+ $date = self::format_date($announcement->post_date, $format);
+ return $date;
+ case 'resource_count':
+ $resources = tutor_utils()->get_attachments($course_id);
+ if (empty($resources)) {
+ return 0;
+ }
+
+ $resource_count = count($resources);
+ return $resource_count;
+ case 'lesson_count':
+ case 'quiz_count':
+ case 'assignments_count':
+ $obj = HelperFunctions::get_posts(
+ array(
+ 'name' => 'topics',
+ 'inherit' => true,
+ 'post_parent' => $course_id,
+ 'item_per_page' => -1,
+ )
+ );
+ if (empty($obj['data'])) {
+ return 0;
+ }
+
+ $count = 0;
+ foreach ($obj['data'] as $topic) {
+ $topic_id = $topic->ID;
+ $topic_contents = self::get_topic_meta($topic_id, $meta_type);
+ $count += count($topic_contents);
+ }
+ return $count;
+ case 'resource_title':
+ $resource = (object) $options['resources'];
+ if (! isset($resource)) {
+ return '';
+ }
+
+ return $resource->name;
+ case 'resource_url':
+ $resource = (object) $options['resources'];
+ if (! isset($resource)) {
+ return '';
+ }
+
+ return $resource->url;
+ case 'resource_size':
+ $resource = (object) $options['resources'];
+ if (! isset($resource)) {
+ return '';
+ }
+
+ return $resource->size;
+ case 'course_benefits':
+ $course_benefits = tutor_course_benefits($course_id);
+ if (empty($course_benefits)) {
+ return array();
+ }
+ return $course_benefits;
+ case 'course_materials':
+ $materials = tutor_course_material_includes($course_id);
+ if (empty($materials)) {
+ return array();
+ }
+ return $materials;
+ case 'course_requirements':
+ $course_requirements = tutor_course_requirements($course_id);
+ if (empty($course_requirements)) {
+ return array();
+ }
+ return $course_requirements;
+ case 'course_target_audience':
+ $course_target_audience = tutor_course_target_audience($course_id);
+ if (empty($course_target_audience)) {
+ return array();
+ }
+ return $course_target_audience;
+ case 'instructor_active_stars':
+ case 'instructor_inactive_stars':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ $instructor_rating = tutor_utils()->get_instructor_ratings($user_id);
+ $rating = (int) round($instructor_rating->rating_avg);
+ return $meta_type === 'instructor_active_stars' ? $rating : 5 - $rating;
+ case 'instructor_course_count':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ $course_count = CourseModel::get_course_count_by_instructor($user_id);
+ return $course_count;
+ case 'instructor_student_count':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ $student_count = tutor_utils()->get_total_students_by_instructor($user_id);
+ return $student_count;
+ case 'instructor_rating':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ $instructor_rating = tutor_utils()->get_instructor_ratings($user_id);
+ $rating = $instructor_rating->rating_avg;
+ return number_format($rating, 1);
+ case 'instructor_rating_count':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ $instructor_rating = tutor_utils()->get_instructor_ratings($user_id);
+ return $instructor_rating->rating_count;
+ case 'instructor_name':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ if (! $is_instructor && $course_id) {
+ $post = get_post($course_id);
+ $user_id = $post->post_author;
+ }
+ $user = tutor_utils()->get_tutor_user($user_id);
+ if (! $user) {
+ return '';
+ }
+ return $user->display_name;
+ case 'instructor_job_title':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ if (! $is_instructor && $course_id) {
+ $post = get_post($course_id);
+ $user_id = $post->post_author;
+ }
+ $user = tutor_utils()->get_tutor_user($user_id);
+ if (! $user) {
+ return '';
+ }
+ return $user->tutor_profile_job_title;
+ case 'instructor_email':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ if (! $is_instructor && $course_id) {
+ $post = get_post($course_id);
+ $user_id = $post->post_author;
+ }
+ $user = tutor_utils()->get_tutor_user($user_id);
+ if (! $user) {
+ return '';
+ }
+ return $user->user_email;
+ case 'instructor_image':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ if (! $is_instructor && $course_id) {
+ $post = get_post($course_id);
+ $user_id = $post->post_author;
+ }
+ $user = tutor_utils()->get_tutor_user($user_id);
+ if (! $user) {
+ return array(
+ 'src' => '',
+ 'alt' => '',
+ );
+ }
+ $image_url = array(
+ 'src' => get_avatar_url($user->ID),
+ 'alt' => $user->display_name,
+ );
+ return $image_url;
+ case 'instructor_bio':
+ $user_id = $course_id;
+ if (isset($options['user']) && isset($options['user']['ID'])) {
+ $user_id = $options['user']['ID'];
+ }
+ if (! $is_instructor && $course_id) {
+ $post = get_post($course_id);
+ $user_id = $post->post_author;
+ }
+ $user = tutor_utils()->get_tutor_user($user_id);
+ if (! $user) {
+ return '';
+ }
+ $instructor_bio = $user->tutor_profile_bio;
+ return $instructor_bio;
+ case 'topic_duration':
+ $topic_contents = tutor_utils()->get_course_contents_by_topic($course_id, -1);
+ if (! isset($topic_contents) || ! isset($topic_contents->posts)) {
+ return [];
+ }
+ $topic_contents = $topic_contents->posts;
+ $hours_sum = 0;
+ $minutes_sum = 0;
+ $seconds_sum = 0;
+ $hours = 0;
+ $minutes = 0;
+ $seconds = 0;
+ foreach ($topic_contents as $content) {
+ $duration = self::get_material_duration($content);
+ if (! $duration) {
+ continue;
+ }
+
+ $duration = explode(':', $duration);
+ $hours = count($duration) === 3 ? (int) $duration[0] : 0;
+ $minutes = count($duration) >= 2 ? (int) $duration[count($duration) - 2] : 0;
+ $seconds = $duration[count($duration) - 1];
+
+ $hours_sum += (int) $hours;
+ $minutes_sum += (int) $minutes;
+ $seconds_sum += (int) $seconds;
+ }
+
+ $hours += (int) ($minutes_sum / 60);
+ $minutes = (int) ($minutes_sum % 60);
+ $minutes += (int) ($seconds_sum / 60);
+ $seconds = (int) ($seconds_sum % 60);
+
+ $duration = '';
+ if ($hours > 0) {
+ $duration .= $hours . ' hr ';
+ }
+
+ if ($minutes > 0) {
+ $duration .= $minutes . ' min ';
+ }
+
+ // if ($seconds > 0) $duration .= $seconds . ' sec';
+ $duration = trim($duration);
+ return $duration;
+ case 'topic_lesson_count':
+ case 'topic_quiz_count':
+ case 'topic_assignments_count':
+ $count = 0;
+ $item = self::get_topic_meta($course_id, str_replace('topic_', '', $meta_type));
+ if (is_array($item)) {
+ $count = count($item);
+ }
+
+ return $count;
+
+ case 'cart_subtotal': {
+ return tutor_get_formatted_price(self::get_cart_subtotal());
+ }
+
+ case 'cart_grand_total': {
+ return tutor_get_formatted_price(self::get_cart_grand_total());
+ }
+
+ case 'net_payment':
+ case 'discount_amount':
+ case 'coupon_amount':
+ case 'tax_rate':
+ case 'tax_amount': {
+ $order_id = Input::post('order_id'); // TODO: get id from dynamic data
+ $key = $meta_type;
+
+ if (! $order_id) return $key;
+
+ return self::get_order_data($order_id, $key);
+ }
+
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Get Tutor Cart Net Payment
+ *
+ * @param int $order_id
+ * @param string $key
+ *
+ * @return float || null
+ */
+
+ // MAKE A COMMON FUNCTION FOR GETTING ORDERS INFO
+ public static function get_order_data($order_id, $key = null)
+ {
+ $order_model = new OrderModel();
+ $order = $order_model->get_order_by_id($order_id);
+
+ if ($order && $key) {
+ return isset($order->$key) ? $order->$key : null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get Tutor Cart Grand Total
+ *
+ * @return float
+ */
+ public static function get_cart_grand_total()
+ {
+ $subtotal = self::get_cart_subtotal();
+
+ $is_tax_included_in_price = Tax::is_tax_included_in_price();
+ $tax_rate = Tax::get_user_tax_rate();
+ $tax_amount = Tax::calculate_tax($subtotal, $tax_rate);
+ $grand_total = $subtotal;
+
+ if (! $is_tax_included_in_price) {
+ $grand_total += $tax_amount;
+ }
+
+ return $grand_total;
+ }
+
+ /**
+ * Get Tutor Cart Subtotal
+ *
+ * @return float
+ */
+ public static function get_cart_subtotal()
+ {
+ $cart_controller = new CartController();
+ $get_cart = $cart_controller->get_cart_items();
+ $courses = $get_cart['courses'];
+ $course_list = $courses['results'];
+
+ $subtotal = 0;
+
+ if (! empty($course_list)) {
+ foreach ($course_list as $course) {
+ $course_price = tutor_utils()->get_raw_course_price($course->ID);
+
+ $regular_price = $course_price->regular_price;
+ $sale_price = $course_price->sale_price;
+
+ $subtotal += $sale_price ? $sale_price : $regular_price;
+ }
+ }
+
+ return $subtotal;
+ }
+
+ /**
+ * Get Tutor Reviews
+ *
+ * @return array
+ */
+ public static function get_course_reviews($course_id, $options = array())
+ {
+ $per_page = tutor_utils()->get_option('pagination_per_page', 10);
+ $offset = 0; // TODO: need to check tutor functionality.
+ $current_user_id = get_current_user_id();
+ $reviews = tutor_utils()->get_course_reviews($course_id, $offset, $per_page, false, array('approved'), $current_user_id);
+ return $reviews;
+ }
+
+ public static function get_topic_meta($topic_id, $meta_type)
+ {
+ $topic_contents = tutor_utils()->get_course_contents_by_topic($topic_id, -1);
+ if (! isset($topic_contents) || ! isset($topic_contents->posts)) {
+ return array();
+ }
+ $topic_contents = $topic_contents->posts;
+ $topic_contents = array_filter(
+ $topic_contents,
+ function ($content) use ($meta_type) {
+ $type = str_replace('_count', '', $meta_type);
+ $type = $type === 'lesson' ? $type : 'tutor_' . $type;
+ return $content->post_type === $type;
+ }
+ );
+ return $topic_contents;
+ }
+
+ public static function get_material_duration($material)
+ {
+ if ($material->post_type !== 'lesson') {
+ return '';
+ }
+ $video = tutor_utils()->get_video_info($material->ID);
+ if (! $video || ! $video->playtime) {
+ return '';
+ }
+ $duration = tutor_utils()->get_optimized_duration($video->playtime);
+ return $duration;
+ }
+
+ public static function format_date($date, $format, $current_format = false)
+ {
+ if ($date && $format) {
+ $wp_format = !$current_format ? get_option('date_format') : $current_format;
+ $datetime = DateTime::createFromFormat($wp_format, $date);
+ if ($datetime) {
+ return $datetime->format($format);
+ }
+ }
+ return $date;
+ }
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/ElementGenerator.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/ElementGenerator.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class ElementGenerator
+ *
+ * This class is used to define all helper functions.
+ */
+class ElementGenerator {
+
+ /**
+ * ElementGenerator constructor.
+ */
+ public function __construct() {
+ add_filter( 'kirki_element_generator_' . TDE_APP_PREFIX, array( $this, 'kirki_element_generator' ), 10, 2 );
+ }
+
+ /**
+ * Kirki element generator
+ * This function is used to generate the kirki elements
+ *
+ * @param string $string string.
+ * @param array $props array. //this props contains all the attributes of the kirki element.
+ *
+ * @return string html.
+ */
+ public function kirki_element_generator( $string, $props ) {
+ $preview = new Preview( $props );
+ return $preview->generate_elements();
+ }
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/MaterialGenerator.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/MaterialGenerator.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Lesson view generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class MaterialGenerator
+ * This class is used to define all material generator functions.
+ */
+trait MaterialGenerator {
+
+ /**
+ * Generate material markup
+ *
+ * @return string
+ */
+ private function generate_material_markup() {
+
+ if ( isset( $this->options['post'] ) ) {
+ // $element_block = $this->group_elements_by_element_name();
+ $settings = isset( $this->properties['settings'] ) ? $this->properties['settings'] : array();
+ if ( $this->options['post']->post_type === $settings['type'] ) {
+ return $this->generate_common_element();
+ }
+ }
+ return '';
+ }
+
+}
--- a/tutor/includes/kirki/backend/ElementGenerator/Preview.php
+++ b/tutor/includes/kirki/backend/ElementGenerator/Preview.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * Preview script for html markup generator
+ *
+ * @package tutor-kirki-elements
+ */
+
+namespace TutorLMSKirkiElementGenerator;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Preview
+ * This class is used to define all preview functions.
+ */
+class Preview {
+
+ use CourseMetaGenerator;
+ use AddRatingGenerator;
+ use MaterialGenerator;
+ use ActionsGenerator;
+ use PriceGenerator;
+ use SocialLinkGenerator;
+ use ThumbnailGenerator;
+
+ /**
+ * Kirki element object
+ *
+ * @var array $element | element data.
+ */
+ private $element = array();
+ /**
+ * Kirki all elements object
+ *
+ * @var array $elements | array of elements.
+ *
Frequently Asked Questions
What is CVE-2026-6965?
Overview of the vulnerabilityCVE-2026-6965 is a medium severity vulnerability in the Tutor LMS plugin for WordPress, affecting versions up to and including 3.9.9. It is classified as an Insecure Direct Object Reference (IDOR), allowing authenticated users with instructor-level access to perform unauthorized operations on other instructors’ course content.
How does this vulnerability work?
Mechanism of exploitationThe vulnerability arises from the `get_course_id_by()` function, which trusts the user-supplied `course` GET parameter without validation. This allows an attacker to manipulate course IDs and bypass authorization checks, enabling them to delete or modify content belonging to other instructors.
Who is affected by this vulnerability?
Identifying vulnerable usersAny WordPress site using the Tutor LMS plugin version 3.9.9 or earlier is affected. Specifically, authenticated users with instructor-level privileges can exploit this vulnerability to affect other instructors’ course content.
How can I check if my site is vulnerable?
Steps for verificationTo check if your site is vulnerable, verify the version of the Tutor LMS plugin installed. If it is version 3.9.9 or earlier, your site is at risk. Additionally, review user roles to identify any users with instructor-level access.
What is the recommended fix for this vulnerability?
Updating the pluginThe recommended fix is to update the Tutor LMS plugin to version 3.9.10 or later, which includes a patch for this vulnerability. Regularly updating plugins is essential for maintaining site security.
What does the CVSS score of 5.3 indicate?
Understanding risk levelsA CVSS score of 5.3 indicates a medium severity vulnerability. This means that while it is not the highest risk, it can lead to significant consequences such as unauthorized data deletion and manipulation, especially in multi-instructor environments.
What practical risks does this vulnerability pose?
Potential impacts on course contentExploitation of this vulnerability can lead to unauthorized deletion of lessons, quizzes, and assignments, as well as manipulation of student grades. This can result in data loss and disruption of course integrity, affecting both instructors and students.
How does the proof of concept demonstrate the issue?
Technical details of exploitationThe proof of concept illustrates how an authenticated instructor can craft a request to delete content by manipulating the `course` GET parameter. By setting this parameter to their own course ID, the attacker can bypass checks and perform operations on victim instructors’ content.
What should I do if I cannot update the plugin immediately?
Mitigation strategiesIf immediate updating is not possible, consider restricting access to instructor-level accounts and monitoring user activities closely. Implementing a Web Application Firewall (WAF) with specific rules to block exploit attempts can also help mitigate risks.
What is Insecure Direct Object Reference (IDOR)?
Definition and implicationsInsecure Direct Object Reference (IDOR) is a type of access control vulnerability where an attacker can manipulate input parameters to gain unauthorized access to resources. It often leads to unauthorized actions such as data deletion or modification.
How can I secure my WordPress site against similar vulnerabilities?
Best practices for WordPress securityTo secure your WordPress site, regularly update all plugins and themes, use strong passwords, limit user permissions, and implement security plugins that monitor for vulnerabilities. Additionally, conduct regular security audits to identify and address potential issues.
Where can I find more information about CVE-2026-6965?
Resources for further readingMore information about CVE-2026-6965 can be found on the National Vulnerability Database (NVD) and security advisories related to the Tutor LMS plugin. These resources provide detailed descriptions and recommendations for addressing the vulnerability.
How Atomic Edge Works
Simple Setup. Powerful Security.
Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.
Trusted by Developers & Organizations






