PluginProbe
Tutor LMS – eLearning and online course solution / 3.9.15
Tutor LMS – eLearning and online course solution v3.9.15
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 All 191 releases
← All changes | classes/Course.php +220 -530 trunk3.9.15 View file →
@@ -9,26 +9,22 @@
9 9 */
10 10
11 11 namespace TUTOR;
12 12
13 -defined( 'ABSPATH' ) || exit;
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit;
15 +}
14 16
15 -use Tutor\Components\Button;
16 -use Tutor\Components\Constants\Size;
17 -use Tutor\Components\Constants\Variant;
18 -use Tutor\Components\Progress;
17 +use stdClass;
18 +use TUTOR\Input;
19 19 use Tutor\Ecommerce\Tax;
20 20 use Tutor\Models\QuizModel;
21 21 use Tutor\Helpers\HttpHelper;
22 22 use Tutor\Models\CourseModel;
23 -use Tutor\Components\Tooltip;
24 23 use Tutor\Ecommerce\Ecommerce;
25 -use Tutor\Helpers\DateTimeHelper;
26 24 use Tutor\Traits\JsonResponse;
27 25 use Tutor\Helpers\ValidationHelper;
28 -use Tutor\Models\EnrollmentModel;
29 -use Tutor\Options_V2;
30 -use TUTOR_ASSIGNMENTS\Assignments;
26 +use TutorPro\CourseBundle\Models\BundleModel;
31 27
32 28 /**
33 29 * Course Class
34 30 *
@@ -98,15 +94,8 @@
98 94 const PUBLIC_COURSE_META = '_tutor_is_public_course';
99 95 const COURSE_SETTINGS_META = '_tutor_course_settings';
100 96 const COURSE_LEVEL_META = '_tutor_course_level';
101 97
102 - /**
103 - * Meta keys used to identify and store Tutor course order data.
104 - *
105 - * @since 4.0.0
106 - */
107 - const IS_TUTOR_ORDER_FOR_COURSE_META = '_is_tutor_order_for_course';
108 - const TUTOR_ORDER_FOR_COURSE_ID_META = '_tutor_order_for_course_id_';
109 98
110 99 /**
111 100 * Additional course meta info
112 101 *
@@ -160,8 +149,15 @@
160 149 */
161 150 add_action( 'save_tutor_course', array( $this, 'attach_product_with_course' ), 10, 2 );
162 151
163 152 /**
153 + * Add course level to course settings
154 + *
155 + * @since v.1.4.1
156 + */
157 + add_filter( 'tutor_course_settings_tabs', array( $this, 'add_course_level_to_settings' ) );
158 +
159 + /**
164 160 * Enable Disable Course Details Page Feature
165 161 *
166 162 * @since v.1.4.8
167 163 */
@@ -207,14 +203,9 @@
207 203 * Delete course data after deleted course
208 204 *
209 205 * @since v.1.6.6
210 206 */
211 - add_action(
212 - 'deleted_post',
213 - function ( $post_id ) {
214 - ( new CourseModel() )->delete_course_data( $post_id );
215 - }
216 - );
207 + add_action( 'deleted_post', array( new CourseModel(), 'delete_course_data' ) );
217 208
218 209 /**
219 210 * Delete course data after deleted course
220 211 *
@@ -307,13 +298,9 @@
307 298 add_filter( 'tutor_user_list_args', array( $this, 'user_list_args_for_instructor' ) );
308 299
309 300 add_filter( 'template_include', array( $this, 'handle_password_protected' ) );
310 301 add_action( 'login_form_postpass', array( $this, 'handle_password_submit' ) );
311 - add_filter( 'post_password_required', array( $this, 'bypass_password_for_enrolled' ), 10, 2 );
312 302 add_filter( 'the_preview', array( $this, 'handle_schedule_courses' ) );
313 -
314 - add_action( 'tutor_course_action_btn', array( $this, 'render_course_action_btn' ) );
315 - add_action( 'wp_ajax_tutor_complete_course', array( $this, 'ajax_tutor_complete_course' ) );
316 303 }
317 304
318 305 /**
319 306 * Handle schedule courses preview for instructors.
@@ -355,31 +342,8 @@
355 342 }
356 343 }
357 344
358 345 /**
359 - * Bypass password protection for enrolled users.
360 - *
361 - * @since 4.0.0
362 - *
363 - * @param bool $required Whether the password is required.
364 - * @param \WP_Post $post The post object.
365 - *
366 - * @return bool false if the current user is enrolled, original value otherwise.
367 - */
368 - public function bypass_password_for_enrolled( $required, $post ) {
369 - if ( ! $required ) {
370 - return $required;
371 - }
372 -
373 - $post_types = array( tutor()->course_post_type, tutor()->bundle_post_type );
374 - if ( in_array( $post->post_type, $post_types, true ) && EnrollmentModel::is_enrolled( $post->ID ) ) {
375 - return false;
376 - }
377 -
378 - return $required;
379 - }
380 -
381 - /**
382 346 * Handle password protected course/bundle.
383 347 *
384 348 * @since 3.0.0
385 349 *
@@ -917,9 +881,9 @@
917 881
918 882 update_post_meta( $course_id, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_FREE );
919 883
920 884 $link = admin_url( 'admin.php?page=create-course' );
921 - if ( tutor()->has_pro && Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
885 + if ( Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
922 886 $link = tutor_utils()->tutor_dashboard_url( 'create-course' );
923 887 }
924 888
925 889 $link = add_query_arg( array( 'course_id' => $course_id ), $link );
@@ -937,14 +901,15 @@
937 901 * Get course list
938 902 *
939 903 * @since 3.0.0
940 904 *
941 - * @since 3.2.0 Refactor the arguments & response as per new design.
905 + * @since 3.2.0
942 906 *
907 + * Refactor the arguments & response as per new design
908 + *
943 909 * @return void
944 910 */
945 911 public function ajax_course_list() {
946 - tutor_utils()->check_nonce();
947 912 $this->check_access();
948 913
949 914 $limit = Input::post( 'limit', 10, Input::TYPE_INT );
950 915 $offset = Input::post( 'offset', 0, Input::TYPE_INT );
@@ -964,9 +929,14 @@
964 929 );
965 930
966 931 $exclude = Input::post( 'exclude', array(), Input::TYPE_ARRAY );
967 932 if ( count( $exclude ) ) {
968 - $exclude = array_filter( $exclude, fn( $id ) => is_numeric( $id ) && $id > 0 );
933 + $exclude = array_filter(
934 + $exclude,
935 + function ( $id ) {
936 + return is_numeric( $id );
937 + }
938 + );
969 939 $args['post__not_in'] = $exclude;
970 940 }
971 941
972 942 $courses = CourseModel::get_courses_by_args( $args );
@@ -1102,17 +1072,12 @@
1102 1072 'course_benefits' => 'sanitize_textarea_field',
1103 1073 'course_target_audience' => 'sanitize_textarea_field',
1104 1074 'course_material_includes' => 'sanitize_textarea_field',
1105 1075 'course_requirements' => 'sanitize_textarea_field',
1106 - ),
1107 - true
1076 + )
1108 1077 );
1109 1078
1110 1079 $course_id = (int) $params['course_id'];
1111 - if ( ! $course_id ) {
1112 - $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1113 - }
1114 -
1115 1080 $this->check_access( $course_id );
1116 1081
1117 1082 $errors = array();
1118 1083 $validation = $this->validate_inputs( $params );
@@ -1201,15 +1166,11 @@
1201 1166 public function ajax_unlink_page_builder() {
1202 1167 tutor_utils()->check_nonce();
1203 1168
1204 1169 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1205 - if ( ! $course_id ) {
1206 - $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1207 - }
1208 -
1170 + $builder = Input::post( 'builder' );
1209 1171 $this->check_access( $course_id );
1210 1172
1211 - $builder = Input::post( 'builder' );
1212 1173 if ( 'elementor' === $builder ) {
1213 1174 delete_post_meta( $course_id, '_elementor_edit_mode' );
1214 1175 } elseif ( 'droip' === $builder ) {
1215 1176 delete_post_meta( $course_id, 'droip_editor_mode' );
@@ -1259,18 +1220,15 @@
1259 1220
1260 1221 $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 );
1261 1222
1262 1223 if ( $topic_contents->have_posts() ) {
1263 - foreach ( $topic_contents->get_posts() as $content ) {
1264 - if ( tutor()->quiz_post_type === $content->post_type ) {
1265 - $questions = tutor_utils()->get_questions_by_quiz( $content->ID );
1266 - $content = (object) array_merge(
1267 - (array) $content,
1268 - array( 'total_question' => is_array( $questions ) ? count( $questions ) : 0 )
1269 - );
1224 + foreach ( $topic_contents->get_posts() as $post ) {
1225 + if ( tutor()->quiz_post_type === $post->post_type ) {
1226 + $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1227 + $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
1270 1228 }
1271 1229
1272 - array_push( $current_topic['contents'], $content );
1230 + array_push( $current_topic['contents'], $post );
1273 1231 }
1274 1232 }
1275 1233
1276 1234 $current_topic = apply_filters( 'tutor_filter_course_content', $current_topic );
@@ -1285,23 +1243,20 @@
1285 1243 /**
1286 1244 * Get course contents
1287 1245 *
1288 1246 * @since 3.0.0
1289 - *
1290 - * @return void
1291 1247 */
1292 1248 public function ajax_course_contents() {
1293 1249 tutor_utils()->check_nonce();
1294 1250
1295 - $errors = array();
1296 1251 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1297 1252
1298 - if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1253 + $this->check_access( $course_id );
1254 +
1255 + if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1299 1256 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1300 1257 }
1301 1258
1302 - $this->check_access( $course_id );
1303 -
1304 1259 if ( ! empty( $errors ) ) {
1305 1260 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1306 1261 }
1307 1262
@@ -1325,14 +1280,14 @@
1325 1280
1326 1281 $errors = array();
1327 1282 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1328 1283
1329 - if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1284 + $this->check_access( $course_id );
1285 +
1286 + if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1330 1287 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1331 1288 }
1332 1289
1333 - $this->check_access( $course_id );
1334 -
1335 1290 if ( ! empty( $errors ) ) {
1336 1291 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1337 1292 }
1338 1293
@@ -1343,9 +1298,9 @@
1343 1298 $price = 0;
1344 1299 $sale_price = 0;
1345 1300 $product_id = tutor_utils()->get_course_product_id( $course_id );
1346 1301
1347 - if ( WooCommerce::MONETIZE_BY === $monetize_by ) {
1302 + if ( 'wc' === $monetize_by ) {
1348 1303 $product = wc_get_product( $product_id );
1349 1304 if ( $product ) {
1350 1305 $product_name = $product->get_name();
1351 1306 $price = $product->get_regular_price();
@@ -1352,9 +1307,9 @@
1352 1307 $sale_price = $product->get_sale_price();
1353 1308 }
1354 1309 }
1355 1310
1356 - if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
1311 + if ( 'tutor' === $monetize_by ) {
1357 1312 $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1358 1313 $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1359 1314 }
1360 1315
@@ -1369,12 +1324,12 @@
1369 1324 $video_intro = get_post_meta( $course_id, '_video', true );
1370 1325 if ( $video_intro ) {
1371 1326 $source = $video_intro['source'] ?? '';
1372 1327 if ( 'html5' === $source ) {
1373 - $poster_url = wp_get_attachment_url( $video_intro['poster'] ?? 0 );
1374 - $source_html5 = wp_get_attachment_url( $video_intro['source_video_id'] ?? 0 );
1375 - $video_intro['poster_url'] = $poster_url;
1376 - $video_intro['source_html5'] = $source_html5;
1328 + $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1329 + $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1330 + $video['poster_url'] = $poster_url;
1331 + $video['source_html5'] = $source_html5;
1377 1332 }
1378 1333 }
1379 1334
1380 1335 $course = get_post( $course_id, ARRAY_A );
@@ -1383,23 +1338,13 @@
1383 1338 }
1384 1339
1385 1340 $editors = tutor_utils()->get_editor_list( $course_id );
1386 1341
1387 - /**
1388 - * Replaced the post_author with current user id if post_author value is 0.
1389 - *
1390 - * @since 4.0.7
1391 - */
1392 - $post_author = (int) $course['post_author'];
1393 - if ( 0 === $post_author && $course_id > 0 ) {
1394 - $post_author = get_current_user_id();
1395 - }
1396 -
1397 1342 $data = array(
1398 1343 'editors' => array_values( $editors ),
1399 1344 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1400 1345 'preview_link' => get_preview_post_link( $course_id ),
1401 - 'post_author' => tutor_utils()->get_tutor_user( $post_author ),
1346 + 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1402 1347 'course_categories' => wp_get_post_terms( $course_id, CourseModel::COURSE_CATEGORY ),
1403 1348 'course_tags' => wp_get_post_terms( $course_id, CourseModel::COURSE_TAG ),
1404 1349 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1405 1350 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
@@ -1539,9 +1484,9 @@
1539 1484 $default_data = ( new Assets( false ) )->get_default_localized_data();
1540 1485
1541 1486 if ( isset( $default_data['current_user']['data']['id'] ) ) {
1542 1487 $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1543 - $default_data['current_user']['data']['tutor_profile_photo_url'] = is_object( $tutor_user ) ? $tutor_user->tutor_profile_photo_url : '';
1488 + $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
1544 1489 }
1545 1490
1546 1491 /**
1547 1492 * Localized only options to protect sensitive info like API keys.
@@ -1546,9 +1491,8 @@
1546 1491 /**
1547 1492 * Localized only options to protect sensitive info like API keys.
1548 1493 */
1549 1494 $required_options = array(
1550 - 'learning_mode',
1551 1495 'monetize_by',
1552 1496 'enable_course_marketplace',
1553 1497 'course_permalink_base',
1554 1498 'supported_video_sources',
@@ -1768,13 +1712,13 @@
1768 1712 $topic_id = (int) isset( $sorting_order[0], $sorting_order[0]['topic_id'] ) ? $sorting_order[0]['topic_id'] : 0;
1769 1713 $course_id = wp_get_post_parent_id( $topic_id );
1770 1714
1771 1715 if ( ! $topic_id || ! $course_id ) {
1772 - $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
1716 + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
1773 1717 }
1774 1718
1775 - if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) && ! User::is_admin() ) {
1776 - $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_UNAUTHORIZED );
1719 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ! User::is_admin() ) {
1720 + wp_send_json_error( tutor_utils()->error_message() );
1777 1721 }
1778 1722
1779 1723 if ( Input::has( 'content_parent' ) ) {
1780 1724 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
@@ -1788,9 +1732,9 @@
1788 1732
1789 1733 // Save course content order.
1790 1734 $this->save_course_content_order( $sorting_order );
1791 1735
1792 - $this->response_success( __( 'Course content order updated successfully!', 'tutor' ) );
1736 + wp_send_json_success();
1793 1737 }
1794 1738
1795 1739 /**
1796 1740 * Restrict new student entry
@@ -1815,11 +1759,9 @@
1815 1759 /**
1816 1760 * Restrict media
1817 1761 *
1818 1762 * @since 1.0.0
1819 - *
1820 1763 * @param string $where where clause.
1821 - *
1822 1764 * @return string
1823 1765 */
1824 1766 public function restrict_media( $where ) {
1825 1767 $action = Input::post( 'action' );
@@ -1850,9 +1792,9 @@
1850 1792 }
1851 1793
1852 1794 $i = 0;
1853 1795 foreach ( $sort_order as $topic ) {
1854 - ++$i;
1796 + $i++;
1855 1797 $wpdb->update(
1856 1798 $wpdb->posts,
1857 1799 array( 'menu_order' => $i ),
1858 1800 array( 'ID' => $topic['topic_id'] )
@@ -1975,9 +1917,8 @@
1975 1917 'source_embedded' => 'wp_kses_post',
1976 1918 ),
1977 1919 true
1978 1920 );
1979 - $video = tutor_utils()->filter_video_meta( $video );
1980 1921 $video_source = tutor_utils()->array_get( 'source', $video );
1981 1922 if ( -1 !== $video_source ) {
1982 1923 update_post_meta( $post_ID, '_video', $video );
1983 1924 } elseif ( ! tutor_is_rest() ) {
@@ -1987,23 +1928,12 @@
1987 1928
1988 1929 /**
1989 1930 * Adding author to instructor automatically
1990 1931 */
1991 - $requested_author_id = Input::post( 'post_author_override', 0, Input::TYPE_INT );
1992 1932
1993 - /**
1994 - * Only accept the requested author override if it targets a real,approved instructor or admin
1995 - *
1996 - * @since 4.0.4
1997 - */
1998 - $author_id = $post->post_author;
1999 - if ( $requested_author_id && ( User::is_admin() || User::is_instructor() ) ) {
2000 - if ( User::is_admin( $requested_author_id ) || User::is_instructor( $requested_author_id, true ) ) {
2001 - $author_id = $requested_author_id;
2002 - }
2003 - }
2004 -
2005 - $attached = (int) $wpdb->get_var(
1933 + // Override post author id.
1934 + $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1935 + $attached = (int) $wpdb->get_var(
2006 1936 $wpdb->prepare(
2007 1937 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
2008 1938 WHERE user_id = %d
2009 1939 AND meta_key = '_tutor_instructor_course_id'
@@ -2028,20 +1958,8 @@
2028 1958 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
2029 1959 }
2030 1960 }
2031 1961
2032 - /**
2033 - * Update course content if main author is changed and multi-instructor addon is disabled.
2034 - *
2035 - * @since 4.0.4
2036 - */
2037 - if ( ! $attached && ! tutor_utils()->is_addon_enabled( 'tutor-multi-instructors' ) ) {
2038 - CourseModel::update_course_content_author( $post_ID, (int) $author_id );
2039 - // Remove all existing instructors from the course and add the new one.
2040 - delete_metadata( 'user', 0, '_tutor_instructor_course_id', $post_ID, true );
2041 - add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
2042 - }
2043 -
2044 1962 do_action( 'tutor_save_course_after', $post_ID, $post );
2045 1963 }
2046 1964
2047 1965 /**
@@ -2200,9 +2118,9 @@
2200 2118 // Process purchase.
2201 2119
2202 2120 } else {
2203 2121 // Free enroll.
2204 - EnrollmentModel::do_enroll( $course_id );
2122 + tutor_utils()->do_enroll( $course_id );
2205 2123 }
2206 2124
2207 2125 $referer_url = wp_get_referer();
2208 2126 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
@@ -2236,9 +2154,9 @@
2236 2154 if ( ! $user_id ) {
2237 2155 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2238 2156 }
2239 2157
2240 - if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2158 + if ( ! tutor_utils()->is_enrolled( $course_id, $user_id ) ) {
2241 2159 die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2242 2160 }
2243 2161
2244 2162 /**
@@ -2245,18 +2163,16 @@
2245 2163 * Filter hook provided to restrict course completion. This is useful
2246 2164 * for specific cases like prerequisites. WP_Error should be returned
2247 2165 * from the filter value to prevent the completion.
2248 2166 */
2249 - $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2167 + $can_complete = apply_filters( 'tutor_user_can_complete_course', true, $user_id, $course_id );
2250 2168
2251 2169 if ( is_wp_error( $can_complete ) ) {
2252 2170 tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2253 - } elseif ( ! $can_complete ) {
2254 - tutor_utils()->redirect_to( $permalink, __( 'You do not have permission to complete this course.', 'tutor' ), 'error' );
2255 2171 } else {
2256 2172 CourseModel::mark_course_as_completed( $course_id, $user_id );
2257 2173 // Set temporary identifier to show review pop up.
2258 - self::set_review_popup_data( $user_id, $course_id );
2174 + self::set_review_popup_data( $user_id, $course_id, $permalink );
2259 2175
2260 2176 wp_safe_redirect( $permalink );
2261 2177 exit;
2262 2178 }
@@ -2262,52 +2178,8 @@
2262 2178 }
2263 2179 }
2264 2180
2265 2181 /**
2266 - * Ajax course complete handler
2267 - *
2268 - * @since 4.0.0
2269 - *
2270 - * @return void
2271 - */
2272 - public function ajax_tutor_complete_course() {
2273 - $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2274 -
2275 - // Checking nonce.
2276 - if ( ! tutor_utils()->is_nonce_verified() ) {
2277 - $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
2278 - }
2279 -
2280 - if ( ! $course_id ) {
2281 - $this->response_bad_request( __( 'Invalid course', 'tutor' ) );
2282 - }
2283 -
2284 - $user_id = get_current_user_id();
2285 - if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2286 - $this->response_bad_request( __( 'You are not enrolled in this course', 'tutor' ) );
2287 - }
2288 -
2289 - /**
2290 - * Filter hook provided to restrict course completion. This is useful
2291 - * for specific cases like prerequisites. WP_Error should be returned
2292 - * from the filter value to prevent the completion.
2293 - */
2294 - $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2295 -
2296 - if ( is_wp_error( $can_complete ) ) {
2297 - $this->response_bad_request( $can_complete->get_error_message() );
2298 - } elseif ( ! $can_complete ) {
2299 - $this->response_bad_request( __( 'You do not have permission to complete this course.', 'tutor' ) );
2300 - } else {
2301 - CourseModel::mark_course_as_completed( $course_id, $user_id );
2302 - // Set temporary identifier to show review pop up.
2303 - self::set_review_popup_data( $user_id, $course_id );
2304 -
2305 - $this->response_success( __( 'Course completed successfully', 'tutor' ) );
2306 - }
2307 - }
2308 -
2309 - /**
2310 2182 * Set data for review popup.
2311 2183 *
2312 2184 * @since 2.2.5
2313 2185 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
@@ -2333,34 +2205,18 @@
2333 2205 * @since 1.0.0
2334 2206 * @return void
2335 2207 */
2336 2208 public function popup_review_form() {
2337 - if ( ! is_user_logged_in() ) {
2338 - return;
2339 - }
2209 + if ( is_user_logged_in() ) {
2210 + $user_id = get_current_user_id();
2211 + $course_id = get_the_ID();
2212 + $meta_key = User::get_review_popup_meta( $course_id );
2213 + $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2340 2214
2341 - $is_learning_area = tutor_utils()->is_learning_area();
2342 - $is_legacy_learning = tutor_utils()->is_legacy_learning_mode();
2343 - if ( $is_legacy_learning && $is_learning_area ) {
2344 - return;
2215 + if ( is_single() && $course_id === $review_course_id ) {
2216 + include tutor()->path . 'views/modal/review.php';
2217 + }
2345 2218 }
2346 -
2347 - $course_id = get_the_ID();
2348 -
2349 - if ( $is_learning_area && ! Input::has( 'subpage' ) ) {
2350 - $course_id = wp_get_post_parent_id( wp_get_post_parent_id( get_the_ID() ) );
2351 - }
2352 -
2353 - if ( empty( $course_id ) ) {
2354 - return;
2355 - }
2356 -
2357 - $user_id = get_current_user_id();
2358 - $meta_key = User::get_review_popup_meta( $course_id );
2359 - $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2360 - if ( is_single() && $course_id === $review_course_id ) {
2361 - include tutor()->path . 'views/modal/review.php';
2362 - }
2363 2219 }
2364 2220
2365 2221 /**
2366 2222 * Review popup data clear
@@ -2388,9 +2244,8 @@
2388 2244 /**
2389 2245 * Delete course delete from frontend dashboard
2390 2246 *
2391 2247 * @since 2.0.0
2392 - *
2393 2248 * @return void
2394 2249 */
2395 2250 public function tutor_delete_dashboard_course() {
2396 2251 tutor_utils()->checking_nonce();
@@ -2409,9 +2264,9 @@
2409 2264 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2410 2265 }
2411 2266
2412 2267 // Check if user is only an instructor.
2413 - if ( ! User::is_admin() ) {
2268 + if ( ! current_user_can( 'administrator' ) ) {
2414 2269 // Check if instructor can trash course.
2415 2270 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2416 2271
2417 2272 if ( ! $can_trash_post ) {
@@ -2421,9 +2276,9 @@
2421 2276
2422 2277 $trash_course = wp_update_post(
2423 2278 array(
2424 2279 'ID' => $course_id,
2425 - 'post_status' => CourseModel::STATUS_TRASH,
2280 + 'post_status' => 'trash',
2426 2281 )
2427 2282 );
2428 2283
2429 2284 if ( $trash_course ) {
@@ -2598,8 +2453,33 @@
2598 2453 }
2599 2454 }
2600 2455
2601 2456 /**
2457 + * Add Course level to course settings
2458 + *
2459 + * @since 1.4.1
2460 + *
2461 + * @param array $args arguments.
2462 + * @return array
2463 + */
2464 + public function add_course_level_to_settings( $args ) {
2465 + $course_id = get_the_ID();
2466 + $levels = tutor_utils()->course_levels();
2467 + $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2468 +
2469 + $args['general']['fields']['_tutor_course_level'] = array(
2470 + 'type' => 'select',
2471 + 'label' => __( 'Difficulty Level', 'tutor' ),
2472 + 'label_title' => __( 'Enable', 'tutor' ),
2473 + 'options' => $levels,
2474 + 'value' => $course_level ? $course_level : 'intermediate',
2475 + 'desc' => __( 'Course difficulty level', 'tutor' ),
2476 + );
2477 +
2478 + return $args;
2479 + }
2480 +
2481 + /**
2602 2482 * Check if course starting
2603 2483 *
2604 2484 * @since 1.4.8
2605 2485 * @return void
@@ -2619,9 +2499,8 @@
2619 2499 /**
2620 2500 * Add Course level to course settings
2621 2501 *
2622 2502 * @since 1.4.8
2623 - *
2624 2503 * @return void
2625 2504 */
2626 2505 public function course_elements_enable_disable() {
2627 2506 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
@@ -2654,13 +2533,12 @@
2654 2533 *
2655 2534 * @since 1.4.8
2656 2535 *
2657 2536 * @param string $html HTML string.
2658 - *
2659 2537 * @return string
2660 2538 */
2661 2539 public function enable_disable_material_includes( $html ) {
2662 - $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true );
2540 + $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2663 2541 if ( $disable_option ) {
2664 2542 return '';
2665 2543 }
2666 2544 return $html;
@@ -2671,9 +2549,8 @@
2671 2549 *
2672 2550 * @since 1.4.8
2673 2551 *
2674 2552 * @param string $html HTML string.
2675 - *
2676 2553 * @return string
2677 2554 */
2678 2555 public function enable_disable_course_content( $html ) {
2679 2556 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
@@ -2688,9 +2565,8 @@
2688 2565 *
2689 2566 * @since 1.4.8
2690 2567 *
2691 2568 * @param string $html HTML string.
2692 - *
2693 2569 * @return string
2694 2570 */
2695 2571 public function enable_disable_course_benefits( $html ) {
2696 2572 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
@@ -2705,9 +2581,8 @@
2705 2581 *
2706 2582 * @since 1.4.8
2707 2583 *
2708 2584 * @param string $html HTML string.
2709 - *
2710 2585 * @return string
2711 2586 */
2712 2587 public function enable_disable_course_requirements( $html ) {
2713 2588 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
@@ -2722,9 +2597,8 @@
2722 2597 *
2723 2598 * @since 1.4.8
2724 2599 *
2725 2600 * @param string $html HTML string.
2726 - *
2727 2601 * @return string
2728 2602 */
2729 2603 public function enable_disable_course_target_audience( $html ) {
2730 2604 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
@@ -2769,9 +2643,9 @@
2769 2643 unset( $items['reviews'] );
2770 2644 }
2771 2645
2772 2646 // Whether enrollment require.
2773 - $is_enrolled = EnrollmentModel::is_enrolled();
2647 + $is_enrolled = tutor_utils()->is_enrolled();
2774 2648
2775 2649 return array_filter(
2776 2650 $items,
2777 2651 function ( $item ) use ( $is_enrolled ) {
@@ -2786,9 +2660,8 @@
2786 2660 /**
2787 2661 * Filter product in shop page
2788 2662 *
2789 2663 * @since 1.4.9
2790 - *
2791 2664 * @return void|null
2792 2665 */
2793 2666 public function filter_product_in_shop_page() {
2794 2667 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
@@ -2794,11 +2667,11 @@
2794 2667 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2795 2668 if ( ! $hide_course_from_shop_page ) {
2796 2669 return;
2797 2670 }
2798 - add_filter( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2799 - add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10 );
2800 - add_filter( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2671 + add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2672 + add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2673 + add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2801 2674 }
2802 2675
2803 2676
2804 2677 /**
@@ -2804,9 +2677,8 @@
2804 2677 /**
2805 2678 * Tutor product meta query
2806 2679 *
2807 2680 * @since 1.4.9
2808 - *
2809 2681 * @return array
2810 2682 */
2811 2683 public function tutor_product_meta_query() {
2812 2684 $meta_query = array(
@@ -2821,9 +2693,8 @@
2821 2693 *
2822 2694 * @since 1.4.9
2823 2695 *
2824 2696 * @param \WP_Query $wp_query WP Query instance.
2825 - *
2826 2697 * @return \WP_Query
2827 2698 */
2828 2699 public function filter_woocommerce_product_query( $wp_query ) {
2829 2700 $product_ids = $this->get_connected_wc_product_ids();
@@ -2865,9 +2736,8 @@
2865 2736 *
2866 2737 * @since 1.4.9
2867 2738 *
2868 2739 * @param \WP_Query $query WP Query instance.
2869 - *
2870 2740 * @return \WP_Query
2871 2741 */
2872 2742 public function filter_edd_downloads_query( $query ) {
2873 2743 $query['meta_query'][] = $this->tutor_product_meta_query();
@@ -2879,9 +2749,8 @@
2879 2749 *
2880 2750 * @since 1.4.9
2881 2751 *
2882 2752 * @param \WP_Query $wp_query WP Query instance.
2883 - *
2884 2753 * @return \WP_Query
2885 2754 */
2886 2755 public function filter_archive_meta_query( $wp_query ) {
2887 2756 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
@@ -2895,9 +2764,8 @@
2895 2764 *
2896 2765 * @since 1.5.8
2897 2766 *
2898 2767 * @param string $html HTML string.
2899 - *
2900 2768 * @return string
2901 2769 */
2902 2770 public function remove_price_if_enrolled( $html ) {
2903 2771 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
@@ -2903,9 +2771,9 @@
2903 2771 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2904 2772
2905 2773 if ( $should_removed ) {
2906 2774 $course_id = get_the_ID();
2907 - $enrolled = EnrollmentModel::is_enrolled( $course_id );
2775 + $enrolled = tutor_utils()->is_enrolled( $course_id );
2908 2776 if ( $enrolled ) {
2909 2777 $html = '';
2910 2778 }
2911 2779 }
@@ -2912,119 +2780,118 @@
2912 2780 return $html;
2913 2781 }
2914 2782
2915 2783 /**
2916 - * Get course completion missing requirements message.
2784 + * Check if all lessons and quizzes done before mark course complete.
2917 2785 *
2918 - * @since 4.0.0
2786 + * @since 1.5.8
2919 2787 *
2920 - * @param int $course_id Course ID.
2921 - * @param int $user_id User ID.
2922 - *
2923 - * @return string|null
2788 + * @param string $html HTML string.
2789 + * @return string
2924 2790 */
2925 - public static function get_course_completion_restrict_msg( $course_id = 0, $user_id = 0 ) {
2926 - $course_id = tutor_utils()->get_post_id( $course_id );
2927 - $user_id = tutor_utils()->get_user_id( $user_id );
2791 + public function tutor_lms_hide_course_complete_btn( $html ) {
2928 2792
2929 - if ( 'strict' !== tutor_utils()->get_option( 'course_completion_process' ) ) {
2930 - return null;
2793 + $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2794 + if ( 'strict' !== $completion_mode ) {
2795 + return $html;
2931 2796 }
2932 2797
2933 - $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
2934 - $total_lessons = tutor_utils()->get_lesson_count_by_course( $course_id ); // @phpstan-ignore method.notFound
2798 + $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2799 + $lesson_count = tutor_utils()->get_lesson_count_by_course();
2935 2800
2936 - if ( $completed_lessons < $total_lessons ) {
2937 - return __( 'Complete all lessons to mark this course as complete', 'tutor' );
2801 + if ( $completed_lesson < $lesson_count ) {
2802 + return '<div class="tutor-alert tutor-warning tutor-mt-28">
2803 + <div class="tutor-alert-text">
2804 + <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2805 + <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2806 + </div>
2807 + </div>';
2938 2808 }
2939 2809
2940 - $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
2810 + $quizzes = array();
2811 + $assignments = array();
2941 2812
2942 - $required_quiz_pass_count = 0;
2943 - $required_assignment_pass_count = 0;
2813 + $course_contents = tutor_utils()->get_course_contents_by_id();
2814 + if ( tutor_utils()->count( $course_contents ) ) {
2815 + foreach ( $course_contents as $content ) {
2816 + if ( 'tutor_quiz' === $content->post_type ) {
2817 + $quizzes[] = $content;
2818 + }
2819 + if ( 'tutor_assignments' === $content->post_type ) {
2820 + $assignments[] = $content;
2821 + }
2822 + }
2823 + }
2944 2824
2945 - $is_assignment_addon_enabled = tutor_utils()->is_addon_enabled( 'tutor-assignments' );
2825 + $required_assignment_pass = 0;
2946 2826
2947 - foreach ( $course_contents as $content ) {
2827 + foreach ( $assignments as $row ) {
2948 2828
2949 - if ( tutor()->quiz_post_type === $content->post_type ) {
2950 - if ( ! QuizModel::is_quiz_passed( $content->ID, $user_id ) ) {
2951 - ++$required_quiz_pass_count;
2829 + $assignment_submission = tutor_utils()->is_assignment_submitted( $row->ID );
2830 + $is_reviewed_by_instructor = ! count( $assignment_submission )
2831 + ? false
2832 + : get_comment_meta( $assignment_submission[0]->comment_ID, 'evaluate_time', true );
2833 +
2834 + if ( $assignment_submission && $is_reviewed_by_instructor ) {
2835 + $pass_mark = tutor_utils()->get_assignment_option( $row->ID, 'pass_mark' );
2836 + $has_passed = false;
2837 + foreach ( $assignment_submission as $submission ) {
2838 + $given_mark = (int) get_comment_meta( $submission->comment_ID, 'assignment_mark', true );
2839 + if ( $given_mark >= $pass_mark ) {
2840 + $has_passed = true;
2841 + break;
2842 + }
2952 2843 }
2953 - }
2954 -
2955 - if ( $is_assignment_addon_enabled && tutor()->assignment_post_type === $content->post_type ) {
2956 - if ( ! Assignments::is_assignment_passed( $content->ID, $user_id ) ) {
2957 - ++$required_assignment_pass_count;
2844 + if ( ! $has_passed ) {
2845 + $required_assignment_pass++;
2958 2846 }
2847 + } else {
2848 + $required_assignment_pass++;
2959 2849 }
2960 2850 }
2961 2851
2962 - if ( ! $required_quiz_pass_count && ! $required_assignment_pass_count ) {
2963 - return null;
2964 - }
2852 + $is_quiz_pass = true;
2853 + $required_quiz_pass = 0;
2965 2854
2966 - return self::get_course_completion_requirement_message(
2967 - $required_quiz_pass_count,
2968 - $required_assignment_pass_count
2969 - );
2970 - }
2855 + if ( tutor_utils()->count( $quizzes ) ) {
2856 + foreach ( $quizzes as $quiz ) {
2971 2857
2972 - /**
2973 - * Build missing course completion requirements message.
2974 - *
2975 - * @since 4.0.0
2976 - *
2977 - * @param int $quiz_count Total quiz count.
2978 - * @param int $assignment_count Total assignment count.
2979 - *
2980 - * @return string
2981 - */
2982 - private static function get_course_completion_requirement_message( $quiz_count, $assignment_count ) {
2983 - $quiz_label = _n( 'quiz', 'quizzes', $quiz_count, 'tutor' );
2984 - $assignment_label = _n( 'assignment', 'assignments', $assignment_count, 'tutor' );
2858 + $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2859 + if ( $attempt ) {
2860 + $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2861 + $earned_percentage = QuizModel::calculate_attempt_earned_percentage( $attempt );
2985 2862
2986 - if ( $quiz_count && ! $assignment_count ) {
2987 - return sprintf(
2988 - /* translators: %1$s: item count; %2$s: item label */
2989 - __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2990 - $quiz_count,
2991 - $quiz_label
2992 - );
2863 + if ( $earned_percentage < $passing_grade ) {
2864 + $required_quiz_pass++;
2865 + $is_quiz_pass = false;
2866 + }
2867 + } else {
2868 + $required_quiz_pass++;
2869 + $is_quiz_pass = false;
2870 + }
2871 + }
2993 2872 }
2994 2873
2995 - if ( ! $quiz_count && $assignment_count ) {
2996 - return sprintf(
2997 - /* translators: %1$s: item count; %2$s: item label */
2998 - __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2999 - $assignment_count,
3000 - $assignment_label
3001 - );
3002 - }
2874 + if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2875 + $_msg = '';
2876 + $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2877 + $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
3003 2878
3004 - return sprintf(
3005 - /* translators: %1$s: quiz count; %2$s: quiz label; %3$s: assignment count; %4$s: assignment label */
3006 - __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ),
3007 - $quiz_count,
3008 - $quiz_label,
3009 - $assignment_count,
3010 - $assignment_label
3011 - );
3012 - }
2879 + if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2880 + /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2881 + $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2882 + }
3013 2883
3014 - /**
3015 - * Check if all lessons and quizzes done before mark course complete.
3016 - *
3017 - * @since 1.5.8
3018 - *
3019 - * @param string $html HTML string.
3020 - *
3021 - * @return string
3022 - */
3023 - public function tutor_lms_hide_course_complete_btn( $html ) {
3024 - $_msg = self::get_course_completion_restrict_msg();
2884 + if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2885 + //phpcs:ignore
2886 + $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2887 + }
3025 2888
3026 - if ( $_msg ) {
2889 + if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2890 + /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2891 + $_msg = sprintf( __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str, $required_assignment_pass, $assignment_str );
2892 + }
2893 +
3027 2894 return '<div class="tutor-alert tutor-warning tutor-mt-28">
3028 2895 <div class="tutor-alert-text">
3029 2896 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
3030 2897 <span>' . $_msg . '</span>
@@ -3040,9 +2907,8 @@
3040 2907 *
3041 2908 * @since 1.5.8
3042 2909 *
3043 2910 * @param string $html HTML string.
3044 - *
3045 2911 * @return string
3046 2912 */
3047 2913 public function get_generate_greadbook( $html ) {
3048 2914 if ( ! tutor_utils()->is_completed_course() ) {
@@ -3054,9 +2920,8 @@
3054 2920 /**
3055 2921 * Add social share content in header
3056 2922 *
3057 2923 * @since 1.6.3
3058 - *
3059 2924 * @return void
3060 2925 */
3061 2926 public function social_share_content() {
3062 2927 global $wp_query, $post;
@@ -3080,9 +2945,8 @@
3080 2945 *
3081 2946 * @since 1.8.2
3082 2947 *
3083 2948 * @param integer $post_id post ID.
3084 - *
3085 2949 * @return void
3086 2950 */
3087 2951 public function delete_associated_enrollment( $post_id ) {
3088 2952 global $wpdb;
@@ -3093,12 +2957,11 @@
3093 2957 post_id
3094 2958 FROM
3095 2959 {$wpdb->postmeta}
3096 2960 WHERE
3097 - meta_key=%s
2961 + meta_key='_tutor_enrolled_by_order_id'
3098 2962 AND meta_value = %d
3099 2963 ",
3100 - EnrollmentModel::ENROLLMENT_ORDER_ID_META,
3101 2964 $post_id
3102 2965 )
3103 2966 );
3104 2967
@@ -3114,35 +2977,21 @@
3114 2977 /**
3115 2978 * Reset course progress.
3116 2979 *
3117 2980 * @since 1.5.8
3118 - *
3119 2981 * @return void
3120 2982 */
3121 2983 public function tutor_reset_course_progress() {
3122 2984 tutor_utils()->checking_nonce();
3123 - $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3124 - $context = Input::post( 'context', '' );
3125 - $course_reset_progress = tutor_utils()->get_option( 'course_reset_progress', false );
3126 - $course_retake_feature = tutor_utils()->get_option( 'course_retake_feature', false );
2985 + $course_id = Input::post( 'course_id' );
3127 2986
3128 - if ( ! $course_reset_progress && 'learning-area-sidebar' === $context ) {
3129 - $this->response_bad_request( __( 'You do not have permission to reset this course.', 'tutor' ) );
2987 + if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2988 + wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
3130 2989 return;
3131 2990 }
3132 2991
3133 - if ( ! $course_retake_feature && ( 'course-landing' === $context || 'learning-area' === $context ) ) {
3134 - $this->response_bad_request( __( 'You do not have permission to retake this course.', 'tutor' ) );
3135 - return;
3136 - }
3137 -
3138 - if ( ! $course_id || ! is_numeric( $course_id ) || ! EnrollmentModel::is_enrolled( $course_id ) ) {
3139 - $this->response_bad_request( __( 'Invalid Course ID or Access Denied.', 'tutor' ) );
3140 - return;
3141 - }
3142 -
3143 2992 tutor_utils()->delete_course_progress( $course_id );
3144 - $this->json_response( '', array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2993 + wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
3145 2994 }
3146 2995
3147 2996 /**
3148 2997 * Do enroll if guest attempt to enroll and course is free
@@ -3150,9 +2999,9 @@
3150 2999 * @since 1.9.8
3151 3000 *
3152 3001 * @param integer $course_id course ID.
3153 3002 * @param integer $user_id user ID.
3154 - *
3003 +
3155 3004 * @return void
3156 3005 */
3157 3006 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
3158 3007 $course_id = sanitize_text_field( $course_id );
@@ -3160,9 +3009,9 @@
3160 3009
3161 3010 if ( $course_id && $is_allowed ) {
3162 3011 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
3163 3012 if ( ! $is_purchasable ) {
3164 - EnrollmentModel::do_enroll( $course_id, $order_id = 0, $user_id );
3013 + tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
3165 3014 do_action( 'guest_attempt_after_enrollment', $course_id );
3166 3015 }
3167 3016 }
3168 3017 }
@@ -3170,9 +3019,8 @@
3170 3019 /**
3171 3020 * Handle course enrollment
3172 3021 *
3173 3022 * @since 2.1.0
3174 - *
3175 3023 * @return void
3176 3024 */
3177 3025 public function course_enrollment() {
3178 3026 tutor_utils()->checking_nonce();
@@ -3179,46 +3027,46 @@
3179 3027
3180 3028 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3181 3029 $user_id = get_current_user_id();
3182 3030
3183 - if ( ! $course_id || ! $user_id ) {
3184 - wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
3185 - }
3031 + if ( $course_id ) {
3032 + $password_protected = post_password_required( $course_id );
3033 + if ( $password_protected ) {
3034 + wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3035 + }
3186 3036
3187 - $password_protected = post_password_required( $course_id );
3188 - if ( $password_protected ) {
3189 - wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3190 - }
3037 + $course = get_post( $course_id );
3191 3038
3192 - $course = get_post( $course_id );
3039 + if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3040 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3041 + }
3193 3042
3194 - if ( CourseModel::STATUS_PRIVATE === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3195 - wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3196 - }
3043 + /**
3044 + * This check was added to address a security issue where users could
3045 + * enroll in a course via an AJAX call without purchasing it.
3046 + *
3047 + * To prevent this, we now verify whether the course is paid.
3048 + * Additionally, we check if the user is already enrolled, since
3049 + * Tutor's default behavior enrolls users automatically upon purchase.
3050 + *
3051 + * @since 3.9.4
3052 + */
3053 + if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3054 + $is_enrolled = (bool) tutor_utils()->is_enrolled( $course_id, $user_id );
3197 3055
3198 - /**
3199 - * This check was added to address a security issue where users could
3200 - * enroll in a course via an AJAX call without purchasing it.
3201 - *
3202 - * To prevent this, we now verify whether the course is paid.
3203 - * Additionally, we check if the user is already enrolled, since
3204 - * Tutor's default behavior enrolls users automatically upon purchase.
3205 - *
3206 - * @since 3.9.4
3207 - */
3208 - if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3209 - $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id );
3056 + if ( ! $is_enrolled ) {
3057 + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3058 + }
3059 + }
3210 3060
3211 - if ( ! $is_enrolled ) {
3212 - wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3061 + $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
3062 + if ( $enroll ) {
3063 + wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3064 + } else {
3065 + wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3213 3066 }
3214 - }
3215 -
3216 - $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id );
3217 - if ( $enroll ) {
3218 - wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3219 3067 } else {
3220 - wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3068 + wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
3221 3069 }
3222 3070 }
3223 3071
3224 3072 /**
@@ -3397,9 +3245,8 @@
3397 3245 * @since 3.8.2
3398 3246 *
3399 3247 * @param int $post_ID The WordPress post ID of the course.
3400 3248 * @param int $product_id The WooCommerce product ID to associate with the course.
3401 - *
3402 3249 * @return void
3403 3250 */
3404 3251 public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3405 3252
@@ -3424,163 +3271,6 @@
3424 3271
3425 3272 // Set course regular & sale price.
3426 3273 update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3427 3274 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3428 - }
3429 -
3430 - /**
3431 - * Render start/resume button in enrolled course action btn.
3432 - *
3433 - * @since 4.0.0
3434 - *
3435 - * @param int $course_id course id.
3436 - *
3437 - * @return void
3438 - */
3439 - public function render_course_action_btn( int $course_id ) {
3440 - $is_completed = tutor_utils()->is_completed_course( $course_id );
3441 - if ( $is_completed ) {
3442 - $certificate_addon_active = tutor_utils()->is_addon_enabled( 'tutor-certificate' );
3443 - if ( ! $certificate_addon_active ) {
3444 - Button::make()
3445 - ->tag( 'a' )
3446 - ->label( __( 'Completed', 'tutor' ) )
3447 - ->icon( Icon::COMPLETED_CIRCLE )
3448 - ->variant( Variant::PRIMARY )
3449 - ->size( Size::X_SMALL )
3450 - ->attr( 'href', esc_url( get_the_permalink( $course_id ) ) )
3451 - ->render();
3452 - }
3453 - return;
3454 - }
3455 -
3456 - $course_progress = tutor_utils()->get_course_completed_percent( $course_id, 0, true );
3457 - $button_text = $course_progress['completed_percent'] > 0 ? __( 'Resume', 'tutor' ) : __( 'Start', 'tutor' );
3458 - $button_url = tutor_utils()->get_course_first_lesson( $course_id );
3459 -
3460 - if ( ! $button_url ) {
3461 - $button_url = get_the_permalink( $course_id );
3462 - }
3463 -
3464 - Button::make()
3465 - ->tag( 'a' )
3466 - ->label( $button_text )
3467 - ->icon( Icon::PLAY_2 )
3468 - ->variant( Variant::PRIMARY )
3469 - ->size( Size::X_SMALL )
3470 - ->attr( 'href', esc_url( $button_url ) )
3471 - ->render();
3472 - }
3473 -
3474 - /**
3475 - * Get the content for the course completion modal.
3476 - *
3477 - * This method returns HTML for displaying a modal that informs the user
3478 - * that they have not completed all required lessons and assessments. It includes
3479 - * the user's current progress shown as a percentage and a progress bar.
3480 - *
3481 - * @since 4.0.0
3482 - *
3483 - * @param float $course_progress The completion percentage of the course.
3484 - *
3485 - * @return string The rendered HTML content for the modal.
3486 - */
3487 - public static function get_complete_modal_content( float $course_progress = 0 ): string {
3488 - ob_start();
3489 - ?>
3490 - <div>
3491 - <p class="tutor-p3 tutor-text-secondary tutor-text-center tutor-mb-7 tutor-px-11">
3492 - <?php esc_html_e( 'You have not completed all required lessons and assessments. ', 'tutor' ); ?>
3493 - </p>
3494 - <div class="tutor-finish-course-progress tutor-border tutor-p-5 tutor-flex tutor-flex-column tutor-gap-4 tutor-surface-base tutor-rounded-md">
3495 - <div class="tutor-flex tutor-items-center tutor-justify-between">
3496 - <div class="tutor-p3 tutor-text-secondary">
3497 - <?php esc_html_e( 'Your Progress', 'tutor' ); ?>
3498 - </div>
3499 - <div class="tutor-p1 tutor-font-bold">
3500 - <?php echo esc_html( number_format_i18n( $course_progress ) . '%' ); ?>
3501 - </div>
3502 - </div>
3503 - <?php
3504 - Progress::make()->type( 'bar' )->value( $course_progress )->render();
3505 - ?>
3506 - </div>
3507 - </div>
3508 - <?php
3509 - return ob_get_clean();
3510 - }
3511 -
3512 - /**
3513 - * Render the course complete button.
3514 - *
3515 - * Displays a button that allows the user to complete the course. If the course
3516 - * progress is less than 100%, clicking the button shows a modal informing the user
3517 - * that not all requirements are fulfilled. If the course progress is 100%,
3518 - * clicking the button attempts to complete the course.
3519 - *
3520 - * @since 4.0.0
3521 - *
3522 - * @param string $modal_id The HTML ID of the modal to display if not complete.
3523 - * @param int $course_id The ID of the course.
3524 - * @param float $course_progress The current completion percentage of the course.
3525 - * @param string $size The button size.
3526 - * @param string $tooltip Optional. Tooltip message.
3527 - * @param bool $block Optional. Whether the button is full-width.
3528 - *
3529 - * @return void
3530 - */
3531 - public static function render_course_complete_btn( string $modal_id, int $course_id, float $course_progress = 0, string $size = Size::MEDIUM, string $tooltip = '', bool $block = false ): void {
3532 - $button = Button::make()
3533 - ->variant( Variant::SECONDARY )
3534 - ->label( __( 'Complete the Course', 'tutor' ) )
3535 - ->icon( Icon::TICK_MARK )
3536 - ->size( $size )
3537 - ->block( $block )
3538 - ->attr( 'type', 'button' );
3539 -
3540 - if ( ! empty( $tooltip ) ) {
3541 - $button->disabled();
3542 - }
3543 -
3544 - if ( $course_progress < 100 ) {
3545 - $button->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" );
3546 - } else {
3547 - $button->attr( '@click', "handleCourseComplete({$course_id})" );
3548 - $button->attr( ':class', "courseCompleteMutation?.isPending ? 'tutor-btn-loading' : ''" );
3549 - $button->attr( ':disabled', 'courseCompleteMutation?.isPending' );
3550 - }
3551 -
3552 - if ( ! empty( $tooltip ) ) {
3553 - Tooltip::make()
3554 - ->content( $tooltip )
3555 - ->placement( Tooltip::PLACEMENT_BOTTOM )
3556 - ->arrow( Tooltip::ARROW_CENTER )
3557 - ->trigger_element( $button->get() )
3558 - ->render();
3559 - } else {
3560 - $button->render();
3561 - }
3562 - }
3563 -
3564 - /**
3565 - * Render course retake button
3566 - *
3567 - * @since 4.0.0
3568 - *
3569 - * @param string $modal_id Modal id.
3570 - * @param string $size The button size.
3571 - * @param bool $block Optional. Whether the button is full-width.
3572 - *
3573 - * @return void
3574 - */
3575 - public static function render_course_retake_btn( string $modal_id, string $size = Size::MEDIUM, bool $block = false ): void {
3576 - Button::make()
3577 - ->variant( Variant::SECONDARY )
3578 - ->label( __( 'Retake this Course', 'tutor' ) )
3579 - ->icon( Icon::RELOAD_4 )
3580 - ->size( $size )
3581 - ->block( $block )
3582 - ->attr( 'type', 'button' )
3583 - ->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" )
3584 - ->render();
3585 3275 }
3586 3276 }