Skip to content
Merged
18 changes: 15 additions & 3 deletions classes/QuizBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,10 @@ public function save_questions( $quiz_id, $questions ) {
$wpdb->update(
$questions_table,
$question_data,
array( 'question_id' => $question_id )
array(
'question_id' => $question_id,
'quiz_id' => $quiz_id,
)
);
}

Expand All @@ -334,7 +337,10 @@ public function save_questions( $quiz_id, $questions ) {
$wpdb->update(
$questions_table,
array( 'question_order' => $question_order ),
array( 'question_id' => $question_id )
array(
'question_id' => $question_id,
'quiz_id' => $quiz_id,
)
);

// Save question's answers.
Expand Down Expand Up @@ -399,7 +405,7 @@ public function validate_payload( $payload ) {
$errors = array_merge( $errors, $validation->errors );
}

if ( $quiz && ! empty( $questions ) ) {
if ( ! empty( $questions ) ) {
$this->set_current_quiz_questions_answer_ids( $quiz_id );
try {
$this->is_valid_quiz_question_answer_payload( $questions );
Expand Down Expand Up @@ -936,6 +942,7 @@ public function is_valid_quiz_question_answer_payload( array $payload ): bool {
$payload_question_ids = wp_list_pluck( $payload, 'question_id' );
$payload_question_answers = wp_list_pluck( $payload, 'question_answers', 'question_id' );
$is_cb_question = wp_list_pluck( $payload, 'is_cb_question', 'question_id' );

// Remove content bank answers.
$payload_question_answers = array_filter( $payload_question_answers, fn( $question_id ) => ! $is_cb_question[ $question_id ], ARRAY_FILTER_USE_KEY );
$payload_question_answers = array_values( $payload_question_answers );
Expand All @@ -945,6 +952,11 @@ public function is_valid_quiz_question_answer_payload( array $payload ): bool {
$payload_question_ids = array_filter( $payload_question_ids, fn( $id ) => is_numeric( $id ) && ! $is_cb_question[ $id ] );
$payload_answer_ids = array_filter( $payload_answer_ids, fn( $id ) => is_numeric( $id ) );

// For the new quiz current_quiz is null, in this there should not be any numeric question or answer id.
if ( is_null( $this->current_quiz ) && ( count( $payload_question_ids ) || count( $payload_answer_ids ) ) ) {
throw new \Exception( esc_html__( 'Invalid question or answer id found', 'tutor' ), 1 );
}

if ( $this->current_quiz_question_ids ) {
$has_question_diff = array_diff( $payload_question_ids, $this->current_quiz_question_ids );
if ( $has_question_diff ) {
Expand Down
42 changes: 31 additions & 11 deletions classes/RestAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,27 +181,47 @@ public function check_permission( $response, $handler, $request ) {
/**
* Class loading
*
* @since 4.0.8 Only autoloads Tutor REST API classes from the plugin's restapi/
* directory. Arbitrary class names must never resolve to files under
* the plugin root (e.g. Composer packages used as POP gadgets).
*
* @since 1.5.0
*
* @param string $class_name class name to load.
*
* @return void
*/
private function loader( $class_name ) {
if ( ! class_exists( $class_name ) ) {
$class_name = preg_replace(
array( '/([a-z])([A-Z])/', '/\\\/' ),
array( '$1$2', DIRECTORY_SEPARATOR ),
$class_name
);
if ( class_exists( $class_name ) ) {
return;
}

$class_name = str_replace( 'TUTOR' . DIRECTORY_SEPARATOR, 'restapi' . DIRECTORY_SEPARATOR, $class_name );
$file_name = $this->path . $class_name . '.php';
// Only handle namespaced Tutor REST classes.
if ( 0 !== strpos( $class_name, 'TUTOR\\' ) ) {
return;
}

$relative = substr( $class_name, strlen( 'TUTOR\\' ) );
$relative = str_replace( '\\', DIRECTORY_SEPARATOR, $relative );

if ( file_exists( $file_name ) ) {
require_once $file_name;
}
// Block path traversal and unexpected nested paths.
if ( '' === $relative || false !== strpos( $relative, '..' ) || false !== strpos( $relative, DIRECTORY_SEPARATOR ) ) {
return;
}

$restapi_dir = realpath( $this->path . 'restapi' );
if ( false === $restapi_dir ) {
return;
}

$file_name = $restapi_dir . DIRECTORY_SEPARATOR . $relative . '.php';
$real_file = realpath( $file_name );

if ( false === $real_file || 0 !== strpos( $real_file, $restapi_dir . DIRECTORY_SEPARATOR ) ) {
return;
}

require_once $real_file;
}

/**
Expand Down
64 changes: 49 additions & 15 deletions classes/Withdraw.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,38 +187,72 @@ public function withdraw_methods_available() {
* Save Withdraw Method Data
*
* @since 1.2.0
* @since 4.0.8 Harden against object injection: capability check, field whitelist, no esc_sql().
*
* @return void send wp_json response
*/
public function tutor_save_withdraw_account() {
// Checking nonce.
tutor_utils()->checking_nonce();

//phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce already verified
$user_id = get_current_user_id();
$method = sanitize_text_field( tutor_utils()->avalue_dot( 'tutor_selected_withdraw_method', $_POST ) );
if ( ! $method ) {

// Withdraw account settings are for instructors only.
if ( ! tutor_utils()->is_instructor( $user_id ) ) {
wp_send_json_error( array( 'msg' => tutor_utils()->error_message() ) );
}

//phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce already verified
$method = sanitize_key( tutor_utils()->avalue_dot( 'tutor_selected_withdraw_method', $_POST ) );
$available_withdraw_method = $this->withdraw_methods_available();

if ( ! $method || ! isset( $available_withdraw_method[ $method ] ) ) {
wp_send_json_error();
}

$method_data = tutor_utils()->avalue_dot( 'withdraw_method_field.' . $method, $_POST );
$available_withdraw_method = $this->withdraw_methods_all();
$form_fields = $available_withdraw_method[ $method ]['form_fields'] ?? array();
if ( ! is_array( $form_fields ) || empty( $form_fields ) ) {
wp_send_json_error();
}

if ( tutor_utils()->count( $method_data ) ) {
$saved_data = array();
$saved_data['withdraw_method_key'] = $method;
$saved_data['withdraw_method_name'] = tutor_utils()->avalue_dot( $method . '.method_name', $available_withdraw_method );
$method_data = tutor_utils()->avalue_dot( 'withdraw_method_field.' . $method, $_POST );
if ( ! is_array( $method_data ) || ! tutor_utils()->count( $method_data ) ) {
wp_send_json_error();
}

$saved_data = array();
$saved_data['withdraw_method_key'] = $method;
$saved_data['withdraw_method_name'] = $available_withdraw_method[ $method ]['method_name'] ?? '';

foreach ( $method_data as $input_name => $value ) {
$saved_data[ $input_name ]['value'] = esc_sql( sanitize_text_field( $value ) );
$saved_data[ $input_name ]['label'] = tutor_utils()->avalue_dot( $method . ".form_fields.{$input_name}.label", $available_withdraw_method );
foreach ( $form_fields as $input_name => $field ) {
if ( ! array_key_exists( $input_name, $method_data ) ) {
continue;
}

update_user_meta( $user_id, '_tutor_withdraw_method_data', $saved_data );
update_user_meta( $user_id, '_tutor_withdraw_selected_method', $method );
update_user_meta( $user_id, '_tutor_withdraw_method_data_' . $method, $saved_data );
$raw_value = $method_data[ $input_name ];
if ( is_array( $raw_value ) ) {
continue;
}

$field_type = $field['type'] ?? 'text';
$value = 'email' === $field_type
? sanitize_email( wp_unslash( $raw_value ) )
: sanitize_text_field( wp_unslash( $raw_value ) );

$saved_data[ $input_name ] = array(
'value' => $value,
'label' => $field['label'] ?? '',
);
}

if ( count( $saved_data ) <= 2 ) {
wp_send_json_error();
}

update_user_meta( $user_id, '_tutor_withdraw_method_data', $saved_data );
update_user_meta( $user_id, '_tutor_withdraw_selected_method', $method );
update_user_meta( $user_id, '_tutor_withdraw_method_data_' . $method, $saved_data );

$msg = apply_filters( 'tutor_withdraw_method_set_success_msg', __( 'Withdrawal information saved!', 'tutor' ) );
wp_send_json_success( array( 'msg' => $msg ) );
}
Expand Down
8 changes: 7 additions & 1 deletion classes/Withdraw_Requests_List.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace TUTOR;

use Tutor\Models\WithdrawModel;

if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Expand Down Expand Up @@ -129,11 +131,11 @@

$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT count(*) FROM {$withdraw_table} AS withdraw

Check failure on line 134 in classes/Withdraw_Requests_List.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$withdraw_table} at &quot;SELECT count(*) FROM {$withdraw_table} AS withdraw&#10;
INNER JOIN {$user_table} AS user

Check failure on line 135 in classes/Withdraw_Requests_List.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$user_table} at INNER JOIN {$user_table} AS user&#10;
ON user.ID = withdraw.user_id
WHERE withdraw.status = %s
{$date_query}

Check failure on line 138 in classes/Withdraw_Requests_List.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$date_query} at {$date_query}&#10;
AND ( user.user_login LIKE %s OR user.user_nicename LIKE %s OR user.user_email LIKE %s OR user.display_name LIKE %s )
",
$status,
Expand All @@ -157,7 +159,7 @@
tutor_utils()->checking_nonce();

// Check if user is privileged.
if ( ! current_user_can( 'administrator' ) ) {

Check failure on line 162 in classes/Withdraw_Requests_List.php

View workflow job for this annotation

GitHub Actions / WPCS

Capabilities should be used instead of roles. Found &quot;administrator&quot; in function call to current_user_can()
wp_send_json_error( tutor_utils()->error_message() );
}

Expand Down Expand Up @@ -203,7 +205,11 @@
if ( 'rejected' === $status ) {
$withdraw = self::get_withdraw_by_id( $withdraw_id );
if ( $withdraw ) {
$details = unserialize( $withdraw->method_data );
$details = WithdrawModel::safe_unserialize_array( $withdraw->method_data );

if ( empty( $details ) ) {
$details = array();
}

$details['rejects'] = array(
'reject_type' => sanitize_text_field( $reject_type ),
Expand Down Expand Up @@ -244,7 +250,7 @@
$withdraw_table = $wpdb->prefix . 'tutor_withdraws';
return $wpdb->get_row(
$wpdb->prepare(
" SELECT *FROM {$withdraw_table}

Check failure on line 253 in classes/Withdraw_Requests_List.php

View workflow job for this annotation

GitHub Actions / WPCS

Use placeholders and $wpdb-&gt;prepare(); found interpolated variable {$withdraw_table} at &quot; SELECT *FROM {$withdraw_table}&#10;
WHERE withdraw_id = %d
",
$withdraw_id
Expand Down
6 changes: 3 additions & 3 deletions components/SearchFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public function get(): string {

$clear_action = 'GET' === strtoupper( $method )
? 'window.location.href = ' . wp_json_encode( $clear_url )
: "setValue('" . esc_js( $input_name ) . "', ''); \$el.closest('form').submit();";
: 'setValue(' . wp_json_encode( (string) $input_name ) . ', \'\'); $el.closest(\'form\').submit();';

$this->attributes = array_merge(
array(
Expand Down Expand Up @@ -279,8 +279,8 @@ public function get(): string {
name="<?php echo esc_attr( $input_name ); ?>"
placeholder="<?php echo esc_attr( $placeholder ); ?>"
class="tutor-input <?php echo esc_attr( $size ); ?> tutor-input-content-left tutor-input-content-clear"
x-bind="register('<?php echo esc_attr( $input_name ); ?>')"
x-init="$nextTick(() => setValue('<?php echo esc_attr( $input_name ); ?>', '<?php echo esc_attr( $search_value ); ?>'))"
x-bind="register(<?php echo esc_attr( wp_json_encode( (string) $input_name ) ); ?>)"
x-init="$nextTick(() => setValue(<?php echo esc_attr( wp_json_encode( (string) $input_name ) ); ?>, <?php echo esc_attr( wp_json_encode( (string) $search_value ) ); ?>))"
/>

<button
Expand Down
67 changes: 63 additions & 4 deletions models/WithdrawModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,75 @@ public static function get_withdrawals_history( $user_id = 0, $filter = array(),
* @since 1.0.0
*
* @param int $user_id user id.
*
* @return bool|mixed
*/
public static function get_user_withdraw_method( $user_id = 0 ) {
$user_id = tutor_utils()->get_user_id( $user_id );
$account = get_user_meta( $user_id, '_tutor_withdraw_method_data', true );
$account = self::get_user_withdraw_method_meta( $user_id, '_tutor_withdraw_method_data' );

return $account ? $account : false;
}

/**
* Get a withdraw method meta blob as a plain array.
*
* Reads the raw usermeta value and unserializes with objects disabled so
* poisoned serialized payloads cannot materialize PHP objects.
*
* @since 4.0.8
*
* @param int $user_id User ID.
* @param string $meta_key Meta key.
*
* @return array
*/
public static function get_user_withdraw_method_meta( $user_id, $meta_key ) {
global $wpdb;

$user_id = absint( $user_id );
$meta_key = sanitize_key( $meta_key );

if ( ! $user_id || ! $meta_key ) {
return array();
}

$raw = $wpdb->get_var(
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = %s LIMIT 1",
$user_id,
$meta_key
)
);

return self::safe_unserialize_array( $raw );
}

/**
* Unserialize withdraw method data as an array only.
*
* @since 4.0.8
*
* @param mixed $data Serialized string or already-decoded array.
*
* @return array
*/
public static function safe_unserialize_array( $data ) {
if ( is_array( $data ) ) {
return $data;
}

if ( ! is_string( $data ) || '' === $data ) {
return array();
}

if ( $account ) {
return maybe_unserialize( $account );
// Reject object payloads before calling unserialize().
if ( preg_match( '/(^|;|{|})O:\+?[0-9]+:"/', $data ) ) {
return array();
}

return false;
$unserialized = unserialize( $data, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize

return is_array( $unserialized ) ? $unserialized : array();
}
}
3 changes: 1 addition & 2 deletions templates/dashboard/account/settings/withdraw.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@

$get_saved_method_values = function ( $method_id, $user_id ) {
$meta_key = '_tutor_withdraw_method_data_' . $method_id;
$values = get_user_meta( $user_id, $meta_key, true );
$values = maybe_unserialize( $values );
$values = WithdrawModel::get_user_withdraw_method_meta( $user_id, $meta_key );
return is_array( $values ) ? $values : array();
};

Expand Down
2 changes: 1 addition & 1 deletion templates/dashboard/account/withdrawals.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
<?php if ( tutor_utils()->count( $withdral_history->results ) > 0 ) : ?>
<?php
foreach ( $withdral_history->results as $withdrawal ) :
$method_data = maybe_unserialize( $withdrawal->method_data );
$method_data = WithdrawModel::safe_unserialize_array( $withdrawal->method_data );
$method_key = $method_data['withdraw_method_key'] ?? '';
$method_icon = $method_icons[ $method_key ] ?? '';
$method_title = '';
Expand Down
2 changes: 1 addition & 1 deletion templates/dashboard/instructor/registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
<input class="tutor-form-control tutor-input" type="text" name="email" value="<?php echo esc_attr( tutor_utils()->input_old( 'email' ) ); ?>" placeholder="<?php esc_html_e( 'E-Mail', 'tutor' ); ?>" required autocomplete="email">
</div>

<div class="tutor-password-strength-checker tutor-mb-8" x-data="{ show: false, value: '<?php echo esc_attr( tutor_utils()->input_old( 'password' ) ); ?>' }">
<div class="tutor-password-strength-checker tutor-mb-8" x-data="{ show: false, value: '' }">
<div class="tutor-input-field">
<label class="tutor-block tutor-mb-3"><?php esc_html_e( 'Password', 'tutor' ); ?></label>
<div class="tutor-form-wrap" style="position: relative;">
Expand Down
2 changes: 1 addition & 1 deletion templates/dashboard/registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
</div>
</div>

<div class="tutor-password-strength-checker" x-data="{ show: false, value: '<?php echo esc_attr( tutor_utils()->input_old( 'password' ) ); ?>' }">
<div class="tutor-password-strength-checker" x-data="{ show: false, value: '' }">
<div class="tutor-password-field">
<label class="tutor-block tutor-mb-3"><?php esc_html_e( 'Password', 'tutor' ); ?></label>
<div class="tutor-input-field tutor-mb-8" style="position: relative;">
Expand Down
4 changes: 2 additions & 2 deletions views/pages/add_new_instructor.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
</div>

<div class="tutor-option-field">
<input type="password" name="password" value="<?php echo esc_attr( tutor_utils()->input_old( 'password' ) ); ?>" placeholder="<?php esc_attr_e( 'Password', 'tutor' ); ?>">
<input type="password" name="password" value="" placeholder="<?php esc_attr_e( 'Password', 'tutor' ); ?>">
</div>
</div>

Expand All @@ -123,7 +123,7 @@
</div>

<div class="tutor-option-field">
<input type="password" name="password_confirmation" value="<?php echo esc_attr( tutor_utils()->input_old( 'password_confirmation' ) ); ?>" placeholder="<?php esc_attr_e( 'Password Confirmation', 'tutor' ); ?>">
<input type="password" name="password_confirmation" value="" placeholder="<?php esc_attr_e( 'Password Confirmation', 'tutor' ); ?>">
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion views/pages/withdraw_requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<?php foreach ( $withdraw_list->results as $list ) : ?>
<?php
$user_data = get_userdata( $list->user_id );
$details = unserialize( $list->method_data );
$details = WithdrawModel::safe_unserialize_array( $list->method_data );
$alert = ( 'pending' == $list->status ? 'warning' : ( 'rejected' === $list->status ? 'danger' : ( 'approved' === $list->status ? 'success' : 'default' ) ) );
$data_name = isset( $details['account_name']['value'] ) ? $details['account_name']['value'] : $user_data->display_name;
if ( ! $details ) {
Expand Down
Loading