Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3ac9990
`/models` files security audit and fixes
Anindra123 Oct 1, 2026
7704e7d
Fix: typcast to int before placing it in string interpolation for query
Anindra123 Oct 1, 2026
0b0936c
Security Fixes
Anindra123 Oct 1, 2026
13cff07
Merge branch '4.1.1' into security/models
Anindra123 Oct 2, 2026
1813746
Fix: Query hardened for the several model class
Anindra123 Oct 2, 2026
9e185bb
updated abspath check to use shorthand syntax
Anindra123 Oct 6, 2026
a9c9e69
updated abspath check to use shorthand syntax
Anindra123 Oct 6, 2026
5305d1e
Merge pull request #3050 from themeum/models/UserModel
shewa12 Oct 7, 2026
84b34a2
Merge branch '4.2.0' into security/models
Anindra123 Oct 8, 2026
9796bf9
added given review changes
Anindra123 Oct 8, 2026
2c02474
Merge branch '4.2.0' into security/models
Anindra123 Oct 8, 2026
914cba3
Merge branch '4.2.0' into security/models
Anindra123 Oct 14, 2026
1b6007a
Merge branch 'security/models' into models/QuizModel
Anindra123 Oct 14, 2026
38f791f
added requested changes
Anindra123 Oct 14, 2026
016ddd5
Merge branch '4.2.0' into security/models
Anindra123 Oct 14, 2026
ac52486
Merge branch 'security/models' into models/QuizModel
Anindra123 Oct 14, 2026
9b62dbd
added requested changes
Anindra123 Oct 14, 2026
9f1cbda
Merge branch '4.2.0' into security/models
Anindra123 Oct 14, 2026
b97134f
Merge branch 'security/models' into models/fixes
Anindra123 Oct 14, 2026
1b0db45
updated array_filter for negative values and string values
Anindra123 Oct 14, 2026
51d7e4c
Merge branch 'security/models' into models/QuizModel
Anindra123 Oct 14, 2026
f10970f
added negative value check on filter
Anindra123 Oct 14, 2026
a0b0058
Merge branch '4.2.0' into security/models
Anindra123 Oct 22, 2026
16fcecf
Merge branch 'security/models' into models/fixes
Anindra123 Oct 22, 2026
2453f46
Merge branch 'security/models' into models/QuizModel
Anindra123 Oct 22, 2026
8437b12
Merge pull request #3055 from themeum/models/fixes
Anindra123 Oct 22, 2026
e23bcb3
Merge pull request #3053 from themeum/models/QuizModel
Anindra123 Oct 22, 2026
7590083
updated to use fluent query and added test cases
Anindra123 Oct 23, 2026
e8e0588
updated quiz model queries and added unit test
Anindra123 Oct 23, 2026
8991354
added fluent query in
Anindra123 Oct 23, 2026
67c455e
removed unused import
Anindra123 Oct 23, 2026
b931bd9
reverted subquery to original
Anindra123 Oct 26, 2026
aa1928f
Merge branch '4.2.0' into security/models
Anindra123 Oct 27, 2026
edff9ef
Updated Enrollment model query to use DB and added unit tests
Anindra123 Oct 27, 2026
78399b3
Added BillingModel unit test
Anindra123 Oct 27, 2026
f753ae2
update billing model to use DB and added test cases
Anindra123 Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
name: core-development-skill
description: This skill governs all development, bug fixing, and feature work inside the **Tutor LMS** plugin codebase (`github.com/themeum/tutor`). It covers project structure, namespace conventions, coding patterns, PHPDoc standards, available helpers/utilities, security rules, and AI agent instructions.
---

# Tutor LMS — Core Development Skill

## Overview
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
name: wpcs-security-skill
description: Enforces strict WordPress coding standards (WPCS) and security best practices for PHP, JS, CSS, and HTML.
---

# WordPress Coding Standards & Security Skill

## Overview
Expand Down
32 changes: 10 additions & 22 deletions models/BillingModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Tutor\Models;

use Tutor\Helpers\QueryHelper;
use Tutor\Core\DB;

/**
* Billing model class for performing billing functionalities
Expand Down Expand Up @@ -77,12 +77,7 @@ public function get_required_fields() {
* @return int The ID of the inserted row on success, or 0 on failure.
*/
public function insert( $data ) {
global $wpdb;

return QueryHelper::insert(
"{$wpdb->prefix}tutor_customers",
$data,
);
return DB::table( 'tutor_customers' )->insert( $data );
}

/**
Expand All @@ -94,13 +89,14 @@ public function insert( $data ) {
* @return bool True on success, false on failure.
*/
public function update( $data, $where ) {
global $wpdb;
$column = '';
$value = null;
if ( tutor_utils()->count( $where ) ) {
$column = array_keys( $where )[0];
$value = $where[ $column ];
}

return QueryHelper::update(
"{$wpdb->prefix}tutor_customers",
$data,
$where,
);
return DB::table( 'tutor_customers' )->where( $column, $value )->update( $data );
}

/**
Expand All @@ -111,15 +107,7 @@ public function update( $data, $where ) {
* @return object|false The billing info as an object if found, or false if not found.
*/
public function get_info( $user_id ) {
global $wpdb;

$billing_info = QueryHelper::get_row(
"{$wpdb->prefix}tutor_customers",
array(
'user_id' => $user_id,
),
'id'
);
$billing_info = DB::table( 'tutor_customers' )->where( 'user_id', $user_id )->first();
return $billing_info;
}

Expand Down
51 changes: 15 additions & 36 deletions models/CourseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@

namespace Tutor\Models;

defined( 'ABSPATH' ) || exit;

use Tutor\Helpers\DateTimeHelper;
use TUTOR\Icon;
use TUTOR\Course;
use TUTOR\Lesson;
use Tutor\Ecommerce\Tax;
use InvalidArgumentException;
use Tutor\Core\DB;
use Tutor\Helpers\QueryHelper;
use TUTOR\User;
use TUTOR_ASSIGNMENTS\Assignments;
Expand Down Expand Up @@ -162,45 +165,21 @@
* @return array|null|object
*/
public static function get_courses( $excludes = array(), $post_status = array( 'publish' ) ) {
global $wpdb;

$excludes = (array) $excludes;
$exclude_query = '';
$excludes = array_values( array_filter( array_map( 'intval', (array) $excludes ), fn( $id ) => $id > 0 ) );
$post_status = array_intersect( $post_status, self::get_status_list() );

if ( count( $excludes ) ) {
$exclude_query = implode( "','", $excludes );
if ( empty( $post_status ) ) {
$post_status = array( 'publish' );
}

$post_status = array_map(
function ( $element ) {
return "'" . $element . "'";
},
$post_status
);

$post_status = implode( ',', $post_status );
$course_post_type = tutor()->course_post_type;
$query = DB::table( 'posts' )
->select( 'ID', 'post_author', 'post_title', 'post_name', 'post_status', 'menu_order' )
->where_in( 'post_status', $post_status )
->where( 'post_type', self::POST_TYPE )
->where_not_in( 'ID', $excludes );

//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$query = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID,
post_author,
post_title,
post_name,
post_status,
menu_order
FROM {$wpdb->posts}
WHERE post_status IN ({$post_status})
AND ID NOT IN('$exclude_query')
AND post_type = %s;
",
$course_post_type
)
);
//phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

return $query;
return $query->get();
}

/**
Expand Down Expand Up @@ -749,7 +728,7 @@
);

// Check if the current user is an admin.
if ( ! current_user_can( 'administrator' ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
$args['author'] = $current_user->ID;
}

Expand Down Expand Up @@ -1186,7 +1165,7 @@
),
);

$courses = current_user_can( 'administrator' ) ? self::get_courses() : self::get_courses_by_instructor();
$courses = current_user_can( 'manage_options' ) ? self::get_courses() : self::get_courses_by_instructor();
if ( ! empty( $courses ) ) {
foreach ( $courses as $course ) {
$course_options[] = array(
Expand Down Expand Up @@ -1530,7 +1509,7 @@
$args = wp_parse_args( $args, $defaults );

if ( ! $args['course_id'] || ! $args['current_post_id'] || ! $args['current_post_type'] ) {
throw new InvalidArgumentException( __( 'Invalid argument passed', 'tutor' ) );

Check failure on line 1512 in models/CourseModel.php

View workflow job for this annotation

GitHub Actions / WPCS

All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '__'.
}

$has_access = false;
Expand Down
78 changes: 29 additions & 49 deletions models/EnrollmentModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

namespace Tutor\Models;

defined( 'ABSPATH' ) || exit;

use Tutor\Cache\TutorCache;
use Tutor\Core\DB;
use TUTOR\Course;
use Tutor\Helpers\QueryHelper;
use TUTOR\User;
Expand Down Expand Up @@ -280,34 +283,27 @@ public static function get_enrolment_by_enrol_id( $enrol_id = 0 ) {
* @return object|mixed
*/
public static function get_enrolled_data( $user_id = 0, $course_id = 0, $status = self::STATUS_COMPLETED ) {
global $wpdb;
$status_clause = $status ? $wpdb->prepare( 'AND post_status = %s ', $status ) : '';
$user_id = absint( $user_id );
$course_id = absint( $course_id );

if ( ! empty( $status ) && ! in_array( $status, array( self::STATUS_COMPLETED, self::STATUS_CANCEL, self::STATUS_PENDING ), true ) ) {
return null;
}

// If course ID provided, it will return single row data.
if ( $course_id > 0 ) {
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_type = %s
AND post_parent = %d
{$status_clause}
AND post_author = %d;",
self::POST_TYPE,
$course_id,
$user_id
)
);
if ( $course_id > 0 && in_array( get_post_type( $course_id ), array( tutor()->course_post_type, tutor()->bundle_post_type ), true ) ) {
return DB::table( 'posts' )
->where( 'post_type', self::POST_TYPE )
->where( 'post_parent', $course_id )
->where( 'post_author', $user_id )
->where( 'post_status', $status ?? self::STATUS_COMPLETED )
->first();
} else {
// Return all enrolled data by user ID.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_type = %s
{$status_clause}
AND post_author = %d;",
self::POST_TYPE,
$user_id
)
);
return DB::table( 'posts' )
->where( 'post_type', self::POST_TYPE )
->where( 'post_author', $user_id )
->where( 'post_status', $status ?? self::STATUS_COMPLETED )
->get();
}
}

Expand All @@ -324,22 +320,9 @@ public static function get_enrolled_data( $user_id = 0, $course_id = 0, $status
* @return bool
*/
public static function update_enrollments( string $status, array $enrollment_ids, bool $trigger_hook = true ): bool {
global $wpdb;
$enrollment_ids_in = QueryHelper::prepare_in_clause( $enrollment_ids );
$status = 'complete' === $status ? 'completed' : $status;
$post_table = $wpdb->posts;
$status = 'complete' === $status ? 'completed' : $status;

//phpcs:disable
$wpdb->query(
$wpdb->prepare(
"UPDATE {$post_table}
SET post_status = %s
WHERE ID IN ($enrollment_ids_in)
",
$status
)
);
//phpcs:enable
DB::table( 'posts' )->where_in( 'ID', $enrollment_ids )->update( array( 'post_status' => $status ) );

if ( $trigger_hook ) {
// Run action hook.
Expand All @@ -362,13 +345,10 @@ public static function update_enrollments( string $status, array $enrollment_ids
* @return bool
*/
public static function delete_enrollment_record( int $student_id, int $course_id ): bool {
return QueryHelper::delete(
'posts',
array(
'post_author' => $student_id,
'post_parent' => $course_id,
'post_type' => self::POST_TYPE,
)
);
return DB::table( 'posts' )
->where( 'post_author', $student_id )
->where( 'post_parent', $course_id )
->where( 'post_type', self::POST_TYPE )
->delete();
}
}
11 changes: 5 additions & 6 deletions models/OrderModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@

namespace Tutor\Models;

defined( 'ABSPATH' ) || exit;

use DateTime;
use Exception;
use Tutor\Cache\TutorCache;
use Tutor\Components\Badge;
use Tutor\Components\Button;
use Tutor\Components\Constants\Variant;
use TUTOR\Earnings;
use Tutor\Ecommerce\BillingController;
use Tutor\Ecommerce\CheckoutController;
Expand All @@ -25,7 +24,6 @@
use Tutor\Helpers\DateTimeHelper;
use Tutor\Helpers\QueryHelper;
use TUTOR\User;
use TutorPro\Ecommerce\Invoice;

/**
* OrderModel Class
Expand Down Expand Up @@ -421,7 +419,8 @@
}
}
} catch ( \Throwable $th ) {
$wpdb->query( 'ROLLBACK' );
throw new \Exception( $th->getMessage() );

Check failure on line 423 in models/OrderModel.php

View workflow job for this annotation

GitHub Actions / WPCS

All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '$th'.
}
}

Expand Down Expand Up @@ -1095,7 +1094,7 @@
//phpcs:enable

if ( $wpdb->last_error ) {
throw new \Exception( $wpdb->last_error );

Check failure on line 1097 in models/OrderModel.php

View workflow job for this annotation

GitHub Actions / WPCS

All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '$wpdb'.
} else {
$response['results'] = $results;
$response['total_count'] = is_array( $results ) && count( $results ) ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
Expand Down Expand Up @@ -1393,9 +1392,9 @@
);
//phpcs:enable
} else {
$earning_table = $wpdb->tutor_earnings;
$earning_table = QueryHelper::prepare_table_name( 'earnings' );
if ( $user_id ) {
$user_clause = "AND {$user_id} = (SELECT user_id FROM {$earning_table} WHERE user_id = {$user_id} LIMIT 1)";
$user_clause = $wpdb->prepare( "AND %d = (SELECT user_id FROM {$earning_table} WHERE user_id = %d LIMIT 1)", $user_id, $user_id ); //phpcs:ignore
}

//phpcs:disable
Expand Down Expand Up @@ -1714,7 +1713,7 @@
if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
$item_subtotal = $sale_price;
$item_total = $sale_price;
} else {

Check failure on line 1716 in models/OrderModel.php

View workflow job for this annotation

GitHub Actions / WPCS

If control structure block found as the only statement within an &quot;else&quot; block. Use elseif instead.
// If there's a discount, apply it to the total price.
if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
$item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
Expand All @@ -1739,7 +1738,7 @@
if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
$item_subtotal = $sale_price;
$item_total = $sale_price;
} else {

Check failure on line 1741 in models/OrderModel.php

View workflow job for this annotation

GitHub Actions / WPCS

If control structure block found as the only statement within an &quot;else&quot; block. Use elseif instead.
// If there's a discount, apply it to the total price.
if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
$item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
Expand Down
Loading
Loading