Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/tutor/classes/QuizBuilder.php
+++ b/tutor/classes/QuizBuilder.php
@@ -343,6 +343,10 @@
$errors = array();
$validation = $this->validate_payload( $payload );
+ if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
+ $validation->success = false;
+ $validation->errors['topic_id'][] = tutor_utils()->error_message();
+ }
if ( ! $validation->success ) {
return (object) array(
--- a/tutor/classes/Utils.php
+++ b/tutor/classes/Utils.php
@@ -8069,11 +8069,7 @@
public function has_enrolled_content_access( $content, $object_id = 0, $user_id = 0 ) {
$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 );
--- a/tutor/classes/Withdraw.php
+++ b/tutor/classes/Withdraw.php
@@ -10,6 +10,7 @@
namespace TUTOR;
+use Exception;
use TutorModelsWithdrawModel;
if ( ! defined( 'ABSPATH' ) ) {
@@ -221,103 +222,132 @@
}
/**
- * Handle withdraw request form submit
+ * Handle withdraw request form submit.
*
* @since 1.0.0
*
* @return void
+ *
+ * @throws Exception If any validation fails.
*/
public function tutor_make_an_withdraw() {
global $wpdb;
tutor_utils()->checking_nonce();
- $user_id = get_current_user_id();
- $withdraw_amount = Input::post( 'tutor_withdraw_amount' );
+ $user_id = get_current_user_id();
+ if ( ! tutor_utils()->is_instructor( $user_id ) ) {
+ wp_send_json_error( array( 'msg' => tutor_utils()->error_message() ) );
+ }
- $earning_summary = WithdrawModel::get_withdraw_summary( $user_id );
- $min_withdraw = tutor_utils()->get_option( 'min_withdraw_amount' );
+ $lock_name = 'tutor_withdraw_lock_' . $user_id;
+ $locked = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 10)', $lock_name ) );
- if ( ( $earning_summary->total_pending + $withdraw_amount ) > $earning_summary->available_for_withdraw ) {
+ if ( 1 !== (int) $locked ) {
wp_send_json_error(
array(
- 'msg' => wp_sprintf(
- /* translators: 1: total pending withdraw request 2: available for withdraw */
- __( "You have total %1$s pending withdraw request. You can't make more than %2$s withdraw request at a time", 'tutor' ),
- $earning_summary->total_pending,
- $earning_summary->available_for_withdraw
- ),
+ 'msg' => __( 'Another withdrawal request is in progress. Please try again.', 'tutor' ),
)
);
}
- $saved_withdraw_account = WithdrawModel::get_user_withdraw_method();
- $formatted_min_withdraw_amount = tutor_utils()->tutor_price( $min_withdraw );
-
- if ( ! tutor_utils()->count( $saved_withdraw_account ) ) {
- $no_withdraw_method = apply_filters( 'tutor_no_withdraw_method_msg', __( 'Please save withdraw method ', 'tutor' ) );
- wp_send_json_error( array( 'msg' => $no_withdraw_method ) );
- }
+ try {
+ $withdraw_amount = (float) Input::post( 'tutor_withdraw_amount' );
+ $earning_summary = WithdrawModel::get_withdraw_summary( $user_id );
+ $min_withdraw = (float) tutor_utils()->get_option( 'min_withdraw_amount' );
+
+ if ( ( $earning_summary->total_pending + $withdraw_amount ) > $earning_summary->available_for_withdraw ) {
+ throw new Exception(
+ wp_sprintf(
+ /* translators: 1: total pending withdraw request 2: available for withdraw */
+ __( "You have total %1$s pending withdraw request. You can't make more than %2$s withdraw request at a time", 'tutor' ),
+ $earning_summary->total_pending,
+ $earning_summary->available_for_withdraw
+ )
+ );
+ }
- if ( ( ! is_numeric( $withdraw_amount ) && ! is_float( $withdraw_amount ) ) || $withdraw_amount < $min_withdraw ) {
- /* translators: 1: strong tag start 2: min withdrawal amount 3: strong tag end */
- $required_min_withdraw = apply_filters( 'tutor_required_min_amount_msg', sprintf( __( 'Minimum withdrawal amount is %1$s %2$s %3$s ', 'tutor' ), '<strong>', $formatted_min_withdraw_amount, '</strong>' ) );
- wp_send_json_error( array( 'msg' => $required_min_withdraw ) );
- }
+ $saved_withdraw_account = WithdrawModel::get_user_withdraw_method();
+ $formatted_min_withdraw_amount = tutor_utils()->tutor_price( $min_withdraw );
- if ( $earning_summary->available_for_withdraw < $withdraw_amount ) {
- $insufficient_balence = apply_filters( 'tutor_withdraw_insufficient_balance_msg', __( 'Insufficient balance.', 'tutor' ) );
+ if ( ! tutor_utils()->count( $saved_withdraw_account ) ) {
+ $no_withdraw_method = apply_filters( 'tutor_no_withdraw_method_msg', __( 'Please save withdraw method ', 'tutor' ) );
+ throw new Exception( $no_withdraw_method );
+ }
- wp_send_json_error( array( 'msg' => $insufficient_balence ) );
- }
+ if ( ( ! is_numeric( $withdraw_amount ) && ! is_float( $withdraw_amount ) ) || $withdraw_amount < $min_withdraw ) {
+ /* translators: 1: strong tag start 2: min withdrawal amount 3: strong tag end */
+ $required_min_withdraw = apply_filters( 'tutor_required_min_amount_msg', sprintf( __( 'Minimum withdrawal amount is %1$s %2$s %3$s ', 'tutor' ), '<strong>', $formatted_min_withdraw_amount, '</strong>' ) );
+ throw new Exception( $required_min_withdraw );
+ }
- $date = gmdate( 'Y-m-d H:i:s', tutor_time() );
+ if ( $earning_summary->available_for_withdraw < $withdraw_amount ) {
+ $insufficient_balence = apply_filters( 'tutor_withdraw_insufficient_balance_msg', __( 'Insufficient balance.', 'tutor' ) );
+ throw new Exception( $insufficient_balence );
+ }
- $withdraw_data = apply_filters(
- 'tutor_pre_withdraw_data',
- array(
- 'user_id' => $user_id,
- 'amount' => $withdraw_amount,
- 'method_data' => maybe_serialize( $saved_withdraw_account ),
- 'status' => 'pending',
- 'created_at' => $date,
- )
- );
+ $date = gmdate( 'Y-m-d H:i:s', tutor_time() );
- $date = gmdate( 'Y-m-d H:i:s', tutor_time() );
+ $withdraw_data = apply_filters(
+ 'tutor_pre_withdraw_data',
+ array(
+ 'user_id' => $user_id,
+ 'amount' => $withdraw_amount,
+ 'method_data' => maybe_serialize( $saved_withdraw_account ),
+ 'status' => 'pending',
+ 'created_at' => $date,
+ )
+ );
- $withdraw_data = apply_filters(
- 'tutor_pre_withdraw_data',
- array(
- 'user_id' => $user_id,
- 'amount' => $withdraw_amount,
- 'method_data' => maybe_serialize( $saved_withdraw_account ),
- 'status' => 'pending',
- 'created_at' => $date,
- )
- );
+ do_action( 'tutor_insert_withdraw_before', $withdraw_data );
- do_action( 'tutor_insert_withdraw_before', $withdraw_data );
+ $inserted = $wpdb->insert( $wpdb->prefix . 'tutor_withdraws', $withdraw_data );
+ if ( false === $inserted ) {
+ throw new Exception( __( 'Unable to process withdrawal request. Please try again.', 'tutor' ) );
+ }
- $wpdb->insert( $wpdb->prefix . 'tutor_withdraws', $withdraw_data );
- $withdraw_id = $wpdb->insert_id;
+ $withdraw_id = $wpdb->insert_id;
- do_action( 'tutor_insert_withdraw_after', $withdraw_id, $withdraw_data );
+ do_action( 'tutor_insert_withdraw_after', $withdraw_id, $withdraw_data );
- /**
- * Getting earning and balance data again
- */
- $earning = WithdrawModel::get_withdraw_summary( $user_id );
- $new_available_balance = tutor_utils()->tutor_price( $earning->available_for_withdraw );
+ /**
+ * Getting earning and balance data again
+ */
+ $earning = WithdrawModel::get_withdraw_summary( $user_id );
+ $new_available_balance = tutor_utils()->tutor_price( $earning->available_for_withdraw );
- do_action( 'tutor_withdraw_after' );
+ do_action( 'tutor_withdraw_after' );
- $withdraw_successfull_msg = apply_filters( 'tutor_withdraw_successful_msg', __( 'Withdrawal Request Sent!', 'tutor' ) );
- wp_send_json_success(
- array(
- 'msg' => $withdraw_successfull_msg,
+ $response = array(
+ 'msg' => apply_filters( 'tutor_withdraw_successful_msg', __( 'Withdrawal Request Sent!', 'tutor' ) ),
'available_balance' => $new_available_balance,
- )
- );
+ );
+
+ } catch ( Exception $e ) {
+
+ $response = array(
+ 'error' => true,
+ 'msg' => $e->getMessage(),
+ );
+
+ } finally {
+
+ $wpdb->query(
+ $wpdb->prepare(
+ 'SELECT RELEASE_LOCK(%s)',
+ $lock_name
+ )
+ );
+ }
+
+ if ( ! empty( $response['error'] ) ) {
+ wp_send_json_error(
+ array(
+ 'msg' => $response['msg'],
+ )
+ );
+ }
+
+ wp_send_json_success( $response );
}
}
--- a/tutor/classes/WooCommerce.php
+++ b/tutor/classes/WooCommerce.php
@@ -64,6 +64,7 @@
*/
add_action( 'woocommerce_new_order', array( $this, 'course_placing_order_from_admin' ), 10, 3 );
add_action( 'woocommerce_new_order_item', array( $this, 'course_placing_order_from_customer' ), 10, 3 );
+ add_action( 'woocommerce_order_status_changed', array( $this, 'handle_customer_order_by_block_checkout' ), 9, 3 );
/**
* Order Status Hook
@@ -607,6 +608,15 @@
return;
}
+ /**
+ * Prevent adding earning data when order is created with WC block checkout page.
+ *
+ * @since 3.9.14
+ */
+ if ( $order->has_status( 'checkout-draft' ) ) {
+ return;
+ }
+
$order_type = $order->get_type();
if ( 'shop_subscription' === $order_type ) {
@@ -715,55 +725,118 @@
}
/**
- * Course placing order from customer
+ * Handle checkout order item.
*
- * @since 1.6.7
- *
- * @since 3.8.0 Filter hook: tutor_is_gift_item added
+ * @since 3.9.14
*
- * @param int $item_id item id.
- * @param mixed $item order item.
- * @param int $order_id wc order id.
+ * @param object $order the order object.
+ * @param object $item the order item object.
+ * @param string $context the context of checkout page.
*
* @return void
*/
- public function course_placing_order_from_customer( $item_id, $item, $order_id ) {
- if ( is_admin() ) {
+ private function handle_checkout_order_item( $order, $item, $context = 'classic_checkout' ) {
+ if ( ! $order instanceof WC_Order || ! $item instanceof WC_Order_Item_Product ) {
return;
}
+ $order_id = $order->get_id();
$is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item );
if ( $is_gift_item ) {
return;
}
- $item = new WC_Order_Item_Product( $item );
$product_id = $item->get_product_id();
$if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
if ( $if_has_course ) {
- $order = wc_get_order( $order_id );
-
- /**
- * Get customer ID from from order
- *
- * @since 2.1.7
- */
- $customer_id = $order->get_customer_id();
$course_id = $if_has_course->post_id;
- if ( ! $customer_id && WC()->session->has_session() ) {
+ $customer_id = $order->get_customer_id();
+
+ // Handle course enrollment.
+ if ( ! $customer_id && WC()->session && WC()->session->has_session() ) {
$guest_customer_id = WC()->session->get_customer_unique_id();
update_post_meta( $course_id, self::TUTOR_WC_GUEST_CUSTOMER_ID, $guest_customer_id );
- return;
+ } else {
+ $has_enrollment = tutor_utils()->is_enrolled( $course_id, $customer_id, true );
+ if ( ! $has_enrollment ) {
+ tutor_utils()->do_enroll( $course_id, $order_id, $customer_id );
+ }
}
- $has_enrollment = tutor_utils()->is_enrolled( $course_id, $customer_id, true );
- if ( ! $has_enrollment ) {
- tutor_utils()->do_enroll( $course_id, $order_id, $customer_id );
+ if ( 'block_checkout' === $context ) {
+ $this->add_earning_data( $item->get_id(), $item, $order_id );
}
}
}
+
+ /**
+ * Course placing order from customer
+ *
+ * @since 1.6.7
+ *
+ * @since 3.8.0 Filter hook: tutor_is_gift_item added
+ *
+ * @param int $item_id item id.
+ * @param mixed $item order item.
+ * @param int $order_id wc order id.
+ *
+ * @return void
+ */
+ public function course_placing_order_from_customer( $item_id, $item, $order_id ) {
+ if ( is_admin() ) {
+ return;
+ }
+
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ return;
+ }
+
+ /**
+ * Prevent when order is created with WC block checkout page.
+ *
+ * @since 3.9.14
+ */
+ if ( $order->has_status( 'checkout-draft' ) ) {
+ return;
+ }
+
+ $this->handle_checkout_order_item( $order, $item, 'classic_checkout' );
+ }
+
+ /**
+ * Handle order status change by block checkout page.
+ *
+ * @since 3.9.14
+ *
+ * @param int $order_id WooCommerce order ID.
+ * @param string $status_from Previous order status.
+ * @param string $status_to New order status.
+ *
+ * @return void
+ */
+ public function handle_customer_order_by_block_checkout( $order_id, $status_from, $status_to ) {
+ if ( 'checkout-draft' !== $status_from ) {
+ return;
+ }
+
+ // If transitioning to another draft or invalid status, do nothing.
+ if ( in_array( $status_to, array( 'checkout-draft', 'draft' ), true ) ) {
+ return;
+ }
+
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ return;
+ }
+
+ foreach ( $order->get_items() as $item ) {
+ $this->handle_checkout_order_item( $order, $item, 'block_checkout' );
+ }
+ }
+
/**
* Handle disabling WooCommerce monetization on WooCommerce plugin deactivation
*
--- a/tutor/includes/droip/backend/Ajax.php
+++ b/tutor/includes/droip/backend/Ajax.php
@@ -77,7 +77,7 @@
wp_send_json_error( 'Course not found!' );
}
- if ( $this->is_course_purchasable( $course_id ) ) {
+ if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
wp_send_json_error( 'You cannot enroll in this course without purchasing it first.' );
}
--- a/tutor/includes/kirki/backend/Ajax.php
+++ b/tutor/includes/kirki/backend/Ajax.php
@@ -78,7 +78,7 @@
wp_send_json_error( 'Course not found!' );
}
- if ( $this->is_course_purchasable( $course_id ) ) {
+ if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
wp_send_json_error( 'You cannot enroll in this course without purchasing it first.' );
}
--- a/tutor/templates/global/attachments.php
+++ b/tutor/templates/global/attachments.php
@@ -13,8 +13,8 @@
exit;
}
-$attachments = tutor_utils()->get_attachments();
-$open_mode_view = apply_filters( 'tutor_pro_attachment_open_mode', null ) == 'view' ? ' target="_blank" ' : null;
+$attachments = tutor_utils()->get_attachments();
+$is_view_mode = 'view' === apply_filters( 'tutor_pro_attachment_open_mode', null );
do_action( 'tutor_global/before/attachments' );
@@ -31,7 +31,13 @@
</div>
<div class="tutor-col-auto">
- <a href="<?php echo esc_url( $attachment->url ); ?>" class="tutor-iconic-btn tutor-iconic-btn-secondary tutor-stretched-link" <?php echo esc_attr( $open_mode_view ? $open_mode_view : "download={$attachment->name}" ); ?>>
+ <a href="<?php echo esc_url( $attachment->url ); ?>"
+ class="tutor-iconic-btn tutor-iconic-btn-secondary tutor-stretched-link"
+ <?php if ( $is_view_mode ) : ?>
+ target="_blank" rel="noopener noreferrer"
+ <?php else : ?>
+ download="<?php echo esc_attr( $attachment->name ); ?>"
+ <?php endif; ?>>
<span class="tutor-icon-download" aria-hidden="true"></span>
</a>
</div>
--- a/tutor/tutor.php
+++ b/tutor/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.13
+ * Version: 3.9.14
* 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.13' );
+define( 'TUTOR_VERSION', '3.9.14' );
define( 'TUTOR_FILE', __FILE__ );
/**
--- a/tutor/vendor/composer/installed.php
+++ b/tutor/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'themeum/tutor',
'pretty_version' => 'dev-4.0.0-dev',
'version' => 'dev-4.0.0-dev',
- 'reference' => '995f88d073ca637db9ac2dc15bb245cc7ff969d0',
+ 'reference' => 'bb3c0ebd967b93eb438b45cbc66f74759fe90a76',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'themeum/tutor' => array(
'pretty_version' => 'dev-4.0.0-dev',
'version' => 'dev-4.0.0-dev',
- 'reference' => '995f88d073ca637db9ac2dc15bb245cc7ff969d0',
+ 'reference' => 'bb3c0ebd967b93eb438b45cbc66f74759fe90a76',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),