Skip to content

Commit 3a17fb2

Browse files
committed
Merge branch 'dev' into svgo
2 parents d0ede5a + b86da08 commit 3a17fb2

15 files changed

Lines changed: 222 additions & 65 deletions

File tree

assets/src/js/frontend/learning-area/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import { initializeQna } from './pages/qna';
1212
import { initializeQuizInterface } from './quiz';
1313
import { initializeSidebar } from './sidebar';
1414

15+
const decodePathSegment = (segment: string): string => {
16+
try {
17+
return decodeURIComponent(segment);
18+
} catch {
19+
return segment;
20+
}
21+
};
22+
1523
const initializeLearningArea = () => {
1624
initializeLearningAreaCommon();
1725
initializeCommon();
@@ -20,8 +28,8 @@ const initializeLearningArea = () => {
2028
initializeReviews();
2129
const { pathname, search } = window.location;
2230

23-
// Normalize path segments
24-
const pathSegments = pathname.split('/').filter(Boolean);
31+
// Decode URL-encoded path segments before comparing with configured slugs.
32+
const pathSegments = pathname.split('/').filter(Boolean).map(decodePathSegment);
2533
const { tutorConfig } = window.TutorCore.config;
2634
const { lesson_slug = 'lessons', quiz_slug = 'quizzes' } = tutorConfig || {};
2735

assets/src/scss/frontend/learning-area/pages/_quiz-intro.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
grid-template-columns: 324px 190px 1.5fr 1fr !important;
123123

124124
@include tutor-breakpoint-down(sm) {
125-
grid-template-columns: auto !important;
125+
grid-template-columns: 1fr auto auto auto !important;
126126
}
127127
}
128128

classes/Assets.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ function ( $file ) {
199199
'placeholder_img_src' => tutor_placeholder_img_src(),
200200
'enable_lesson_classic_editor' => get_tutor_option( 'enable_lesson_classic_editor' ),
201201
'tutor_frontend_dashboard_url' => tutor_utils()->get_tutor_dashboard_page_permalink(),
202+
'is_dashboard_page' => tutor_utils()->is_dashboard_page(),
202203
'wp_date_format' => tutor_js_date_format_against_wp(),
203204
'start_of_week' => get_option( 'start_of_week', 1 ),
204205
'is_admin' => is_admin(),

classes/Quiz.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ public static function manage_attempt_answers( $attempt_answers, $attempt, $atte
793793
$is_answer_was_correct = false;
794794
$given_answer = '';
795795

796-
if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
796+
if ( QuizModel::QUESTION_TYPE_TRUE_FALSE === $question_type || QuizModel::QUESTION_TYPE_SINGLE_CHOICE === $question_type ) {
797797
$given_answer = $answers;
798798
$is_answer_was_correct = (bool) $wpdb->get_var(
799799
$wpdb->prepare(
@@ -805,7 +805,7 @@ public static function manage_attempt_answers( $attempt_answers, $attempt, $atte
805805
)
806806
);
807807

808-
} elseif ( 'multiple_choice' === $question_type ) {
808+
} elseif ( QuizModel::QUESTION_TYPE_MULTIPLE_CHOICE === $question_type ) {
809809

810810
$given_answer = (array) ( $answers );
811811
$given_answer = array_filter( $given_answer, fn ( $id ) => is_numeric( $id ) && intval( $id ) > 0 );
@@ -828,9 +828,10 @@ public static function manage_attempt_answers( $attempt_answers, $attempt, $atte
828828
if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
829829
$is_answer_was_correct = true;
830830
}
831-
$given_answer = maybe_serialize( $answers );
832831

833-
} elseif ( 'fill_in_the_blank' === $question_type ) {
832+
$given_answer = maybe_serialize( $given_answer );
833+
834+
} elseif ( QuizModel::QUESTION_TYPE_FILL_IN_THE_BLANK === $question_type ) {
834835

835836
$get_original_answer = $wpdb->get_row(
836837
$wpdb->prepare(
@@ -869,11 +870,11 @@ function ( $ans ) {
869870
if ( strtolower( $given_answer ) === strtolower( $gap_answer ) ) {
870871
$is_answer_was_correct = true;
871872
}
872-
} elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
873+
} elseif ( QuizModel::QUESTION_TYPE_OPEN_ENDED === $question_type || QuizModel::QUESTION_TYPE_SHORT_ANSWER === $question_type ) {
873874
$review_required = true;
874875
$given_answer = wp_kses_post( $answers );
875876

876-
} elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
877+
} elseif ( QuizModel::QUESTION_TYPE_ORDERING === $question_type || QuizModel::QUESTION_TYPE_MATCHING === $question_type || QuizModel::QUESTION_TYPE_IMAGE_MATCHING === $question_type ) {
877878
$answers = (array) tutor_utils()->avalue_dot( 'answers', $answers );
878879

879880
$given_answer = (array) array_map( 'sanitize_text_field', $answers );
@@ -897,7 +898,7 @@ function ( $ans ) {
897898
if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
898899
$is_answer_was_correct = true;
899900
}
900-
} elseif ( 'image_answering' === $question_type ) {
901+
} elseif ( QuizModel::QUESTION_TYPE_IMAGE_ANSWERING === $question_type ) {
901902
$image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
902903
$image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
903904
$given_answer = maybe_serialize( $image_inputs );
@@ -963,7 +964,7 @@ function ( $ans ) {
963964
* Check if question_type open ended or short ans the set
964965
* is_correct default value null before saving
965966
*/
966-
if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
967+
if ( in_array( $question_type, QuizModel::get_manual_review_types(), true ) ) {
967968
$answers_data['is_correct'] = null;
968969
$review_required = true;
969970
}
@@ -1953,6 +1954,10 @@ public static function render_quiz_attempts( $quiz_id ) {
19531954
)
19541955
);
19551956
?>
1957+
1958+
<div class="tutor-quiz-item-actions">
1959+
<?php $quiz_attempt_obj->render_details_button( $attempt, true ); ?>
1960+
</div>
19561961
</div>
19571962
<?php
19581963
}

classes/Quiz_Attempts_List.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -467,20 +467,26 @@ public function render_retry_button( $course_id = 0, $quiz_id = 0, $attempt = ar
467467
* @since 4.0.0
468468
*
469469
* @param array $attempt the quiz attempt.
470+
* @param bool $is_learning_area is learning area list item.
470471
*
471472
* @return void
472473
*/
473-
public function render_details_button( $attempt ) {
474-
if ( User::is_student_view() ) {
475-
Button::make()
476-
->label( __( 'Details', 'tutor' ) )
477-
->icon( Icon::RESOURCES, 'left', 20 )
478-
->size( Size::MEDIUM )
479-
->tag( 'a' )
480-
->attr( 'href', $this->get_review_url( $attempt ) )
481-
->variant( 'primary' )
482-
->render();
474+
public function render_details_button( $attempt, $is_learning_area = false ) {
475+
if ( ! User::is_student_view() ) {
476+
return;
483477
}
478+
479+
$args = $is_learning_area ? array( 'action' => 'view_details' ) : array();
480+
$url = $this->get_review_url( $attempt, $args );
481+
482+
Button::make()
483+
->label( __( 'Details', 'tutor' ) )
484+
->icon( Icon::RESOURCES, 'left', 20 )
485+
->size( Size::MEDIUM )
486+
->tag( 'a' )
487+
->attr( 'href', $url )
488+
->variant( 'primary' )
489+
->render();
484490
}
485491

486492
/**

classes/RestAPI.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,66 @@ public function __construct() {
116116
$this->rating_obj = new REST_Rating();
117117

118118
add_action( 'rest_api_init', array( $this, 'init_routes' ) );
119+
add_filter( 'rest_request_before_callbacks', array( $this, 'check_permission' ), 10, 3 );
120+
}
121+
122+
/**
123+
* Check permission before dispatching REST request.
124+
*
125+
* @todo will remove and prevent by capability where needed.
126+
*
127+
* @since 4.0.1
128+
*
129+
* @param mixed $response response.
130+
* @param mixed $handler handler.
131+
* @param \WP_REST_Request $request request.
132+
*
133+
* @return mixed|\WP_Error
134+
*/
135+
public function check_permission( $response, $handler, $request ) {
136+
$id = absint( $request['id'] );
137+
$method = $request->get_method();
138+
$write_methods = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
139+
$is_write = in_array( $method, $write_methods, true );
140+
141+
if ( ! $id ) {
142+
return $response;
143+
}
144+
145+
$post = get_post( $id );
146+
147+
if ( ! $post ) {
148+
return $response;
149+
}
150+
151+
$post_types = array(
152+
tutor()->course_post_type,
153+
tutor()->bundle_post_type,
154+
);
155+
156+
if ( ! in_array( $post->post_type, $post_types, true ) ) {
157+
return $response;
158+
}
159+
160+
// Prevent write actions which are only allowed for author.
161+
if ( $is_write && ! current_user_can( 'edit_post', $id ) ) {
162+
return new \WP_Error(
163+
'rest_forbidden',
164+
tutor_utils()->error_message(),
165+
array( 'status' => rest_authorization_required_code() )
166+
);
167+
}
168+
169+
// Prevent access to non-publish posts, only author can access.
170+
if ( 'publish' !== $post->post_status && ! current_user_can( 'edit_post', $id ) ) {
171+
return new \WP_Error(
172+
'rest_forbidden',
173+
tutor_utils()->error_message(),
174+
array( 'status' => rest_authorization_required_code() )
175+
);
176+
}
177+
178+
return $response;
119179
}
120180

121181
/**

classes/Utils.php

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9819,7 +9819,7 @@ public function error_message( $key = '401' ) {
98199819
*
98209820
* @since 2.2.4
98219821
*
9822-
* @param string $plugin_slug
9822+
* @param string $plugin_slug plugin slug.
98239823
*
98249824
* @return object|bool if success return object otherwise return false;
98259825
*/
@@ -9832,6 +9832,70 @@ public function get_remote_plugin_info( $plugin_slug = 'tutor' ) {
98329832
return (object) json_decode( $response['body'], true );
98339833
}
98349834

9835+
/**
9836+
* Get the latest available version of a plugin from WordPress.org's update-check API,
9837+
* with optional transient caching.
9838+
*
9839+
* @param string $plugin_file Plugin file path relative to plugins dir, e.g. "tutor/tutor.php".
9840+
* @param bool $use_cache Whether to read/write the transient cache. Default true.
9841+
* @param int $cache_expiry Cache lifetime in seconds. Default 5 minutes.
9842+
*
9843+
* @return string|false Latest version string on success, false if not found / on error.
9844+
*/
9845+
public function get_wp_org_update_version( $plugin_file, $use_cache = true, $cache_expiry = 5 * MINUTE_IN_SECONDS ) {
9846+
9847+
$cache_key = 'wp_org_update_' . md5( $plugin_file );
9848+
9849+
if ( $use_cache ) {
9850+
// Return cached result if available (including cached "not found" results).
9851+
$cached = get_transient( $cache_key );
9852+
if ( false !== $cached ) {
9853+
return ( '__none__' === $cached ) ? false : $cached;
9854+
}
9855+
}
9856+
9857+
$body = array(
9858+
'plugins' => wp_json_encode(
9859+
array(
9860+
'plugins' => array(
9861+
$plugin_file => array(),
9862+
),
9863+
)
9864+
),
9865+
);
9866+
9867+
$response = wp_remote_post(
9868+
'https://api.wordpress.org/plugins/update-check/1.1/',
9869+
array(
9870+
'timeout' => 10,
9871+
'headers' => array(
9872+
'Content-Type' => 'application/x-www-form-urlencoded',
9873+
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url( '/' ),
9874+
),
9875+
'body' => $body,
9876+
)
9877+
);
9878+
9879+
$version = false;
9880+
9881+
if ( ! is_wp_error( $response ) ) {
9882+
$code = wp_remote_retrieve_response_code( $response );
9883+
$data = json_decode( wp_remote_retrieve_body( $response ), true );
9884+
9885+
if ( 200 === (int) $code && ! empty( $data['plugins'][ $plugin_file ]['new_version'] ) ) {
9886+
$version = $data['plugins'][ $plugin_file ]['new_version'];
9887+
}
9888+
}
9889+
9890+
if ( $use_cache ) {
9891+
// Cache the version string, or a "not found" marker so we don't hammer the API on repeated misses.
9892+
set_transient( $cache_key, false === $version ? '__none__' : $version, $cache_expiry );
9893+
}
9894+
9895+
return $version;
9896+
}
9897+
9898+
98359899
/**
98369900
* Get editor list for post content.
98379901
*

classes/WhatsNew.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,7 @@ public function whats_new_page() {
7777
* @return array
7878
*/
7979
private function get_plugin_version_info() {
80-
$transient_key = 'tutor_plugin_info';
81-
$plugin_info = get_transient( $transient_key );
82-
83-
if ( false === $plugin_info ) {
84-
$plugin_info = tutils()->get_remote_plugin_info();
85-
set_transient( $transient_key, $plugin_info, HOUR_IN_SECONDS );
86-
}
87-
88-
$remote_version = $plugin_info->version ?? TUTOR_VERSION;
80+
$remote_version = tutils()->get_wp_org_update_version( 'tutor/tutor.php', true, HOUR_IN_SECONDS );
8981
$installed_version = TUTOR_VERSION;
9082
$update_required = version_compare( $remote_version, $installed_version, '>' );
9183

ecommerce/HooksHandler.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,11 @@ public function manage_earnings_and_enrollments( string $order_status, int $orde
405405
*/
406406
if ( $this->order_model->is_single_order( $order ) ) {
407407
update_post_meta( $has_enrollment->ID, EnrollmentModel::ENROLLMENT_ORDER_ID_META, $order_id );
408-
$has_bundle_enrollment_meta = get_post_meta( $has_enrollment->ID, CourseBundle::BUNDLE_ENROLLMENT_META, true );
409-
if ( $has_bundle_enrollment_meta ) {
410-
delete_post_meta( $has_enrollment->ID, CourseBundle::BUNDLE_ENROLLMENT_META );
408+
if ( tutor_utils()->is_addon_enabled( 'course-bundle' ) ) {
409+
$has_bundle_enrollment_meta = get_post_meta( $has_enrollment->ID, CourseBundle::BUNDLE_ENROLLMENT_META, true );
410+
if ( $has_bundle_enrollment_meta ) {
411+
delete_post_meta( $has_enrollment->ID, CourseBundle::BUNDLE_ENROLLMENT_META );
412+
}
411413
}
412414
/**
413415
* Update enrollment expiry date if it is set in a course.

models/QuizModel.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ public static function get_question_types( $type = '' ) {
193193
* @return array
194194
*/
195195
public static function get_manual_review_types() {
196-
return array( 'open_ended', 'short_answer' );
196+
return array(
197+
self::QUESTION_TYPE_OPEN_ENDED,
198+
self::QUESTION_TYPE_SHORT_ANSWER,
199+
self::QUESTION_TYPE_IMAGE_ANSWERING,
200+
);
197201
}
198202

199203

0 commit comments

Comments
 (0)