PluginProbe
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
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 +3335 -315 1.0.54.0.0 View file →
@@ -1,523 +1,3543 @@
1 1 <?php
2 +/**
3 + * Manage Course Related Logic
4 + *
5 + * @package Tutor
6 + * @author Themeum <support@themeum.com>
7 + * @link https://themeum.com
8 + * @since 1.0.0
9 + */
10 +
2 11 namespace TUTOR;
3 12
4 -if ( ! defined( 'ABSPATH' ) )
5 - exit;
13 +defined( 'ABSPATH' ) || exit;
6 14
15 +use Tutor\Components\Button;
16 +use Tutor\Components\Constants\Size;
17 +use Tutor\Components\Constants\Variant;
18 +use Tutor\Components\Progress;
19 +use Tutor\Ecommerce\Tax;
20 +use Tutor\Models\QuizModel;
21 +use Tutor\Helpers\HttpHelper;
22 +use Tutor\Models\CourseModel;
23 +use Tutor\Components\Tooltip;
24 +use Tutor\Ecommerce\Ecommerce;
25 +use Tutor\Helpers\DateTimeHelper;
26 +use Tutor\Traits\JsonResponse;
27 +use Tutor\Helpers\ValidationHelper;
28 +use Tutor\Models\EnrollmentModel;
29 +use Tutor\Options_V2;
30 +use TUTOR_ASSIGNMENTS\Assignments;
31 +
32 +/**
33 + * Course Class
34 + *
35 + * @since 1.0.0
36 + */
7 37 class Course extends Tutor_Base {
8 - public function __construct() {
38 + use JsonResponse;
39 +
40 + /**
41 + * Course Price type
42 + *
43 + * @since 3.0.0
44 + *
45 + * @var string
46 + */
47 + const PRICE_TYPE_FREE = 'free';
48 + const PRICE_TYPE_PAID = 'paid';
49 + const PRICE_TYPE_SUBSCRIPTION = 'subscription';
50 +
51 + /**
52 + * Course price and sale price
53 + *
54 + * @since 3.0.0
55 + */
56 + const COURSE_PRICE_TYPE_META = '_tutor_course_price_type';
57 + const COURSE_PRICE_META = 'tutor_course_price';
58 + const COURSE_SALE_PRICE_META = 'tutor_course_sale_price';
59 + const COURSE_SELLING_OPTION_META = 'tutor_course_selling_option';
60 + const COURSE_PRODUCT_ID_META = '_tutor_course_product_id';
61 +
62 + /**
63 + * Selling option constants
64 + *
65 + * @since 3.0.0
66 + */
67 + const SELLING_OPTION_ONE_TIME = 'one_time';
68 + const SELLING_OPTION_SUBSCRIPTION = 'subscription';
69 + const SELLING_OPTION_BOTH = 'both'; // onetime and subscription.
70 + const SELLING_OPTION_MEMBERSHIP = 'membership';
71 + const SELLING_OPTION_ALL = 'all';
72 +
73 + /**
74 + * Tax collection settings meta
75 + *
76 + * @since 3.7.0
77 + */
78 + const TAX_ON_SINGLE_META = 'tutor_tax_on_single';
79 + const TAX_ON_SUBSCRIPTION_META = 'tutor_tax_on_subscription';
80 +
81 + /**
82 + * Additional data meta
83 + *
84 + * @since 3.6.0
85 + */
86 + const COURSE_BENEFITS_META = '_tutor_course_benefits';
87 + const COURSE_REQUIREMENTS_META = '_tutor_course_requirements';
88 + const COURSE_TARGET_AUDIENCE_META = '_tutor_course_target_audience';
89 + const COURSE_MATERIAL_INCLUDE_META = '_tutor_course_material_includes';
90 + const COURSE_DURATION_META = '_course_duration';
91 +
92 + /**
93 + * Course settings meta
94 + *
95 + * @since 3.6.0
96 + */
97 + const COURSE_ENABLE_QA_META = '_tutor_enable_qa';
98 + const PUBLIC_COURSE_META = '_tutor_is_public_course';
99 + const COURSE_SETTINGS_META = '_tutor_course_settings';
100 + const COURSE_LEVEL_META = '_tutor_course_level';
101 +
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 +
110 + /**
111 + * Additional course meta info
112 + *
113 + * @var array
114 + */
115 + private $additional_meta = array(
116 + '_tutor_enable_qa',
117 + '_tutor_is_public_course',
118 + );
119 +
120 + /**
121 + * Constructor
122 + *
123 + * @since 1.0.0
124 + * @since 3.0.0 $register_hooks param added to reuse this class.
125 + *
126 + * @param bool $register_hooks register hooks.
127 + *
128 + * @return void
129 + */
130 + public function __construct( $register_hooks = true ) {
9 131 parent::__construct();
10 132
11 - add_action( 'add_meta_boxes', array($this, 'register_meta_box') );
12 - add_action('save_post_'.$this->course_post_type, array($this, 'save_course_meta'), 10, 2);
13 - add_action('wp_ajax_tutor_add_course_topic', array($this, 'tutor_add_course_topic'));
14 - add_action('wp_ajax_tutor_update_topic', array($this, 'tutor_update_topic'));
133 + if ( ! $register_hooks ) {
134 + return;
135 + }
15 136
16 - //Add Column
17 - add_filter( "manage_{$this->course_post_type}_posts_columns", array($this, 'add_column'), 10,1 );
18 - add_action( "manage_{$this->course_post_type}_posts_custom_column" , array($this, 'custom_lesson_column'), 10, 2 );
137 + add_action( 'save_post_' . $this->course_post_type, array( $this, 'save_course_meta' ), 10, 2 );
19 138
20 - add_action('admin_action_tutor_delete_topic', array($this, 'tutor_delete_topic'));
21 - add_action('admin_action_tutor_delete_announcement', array($this, 'tutor_delete_announcement'));
139 + add_action( 'wp_ajax_tutor_save_topic', array( $this, 'tutor_save_topic' ) );
140 + add_action( 'wp_ajax_tutor_delete_topic', array( $this, 'tutor_delete_topic' ) );
22 141
23 - //Frontend Action
24 - add_action('template_redirect', array($this, 'enroll_now'));
25 - add_action('template_redirect', array($this, 'mark_course_complete'));
142 + /**
143 + * Frontend Action
144 + */
145 + add_action( 'template_redirect', array( $this, 'enroll_now' ) );
146 + add_action( 'init', array( $this, 'mark_course_complete' ) );
26 147
27 - //Modal Perform
28 - add_action('wp_ajax_tutor_load_instructors_modal', array($this, 'tutor_load_instructors_modal'));
29 - add_action('wp_ajax_tutor_add_instructors_to_course', array($this, 'tutor_add_instructors_to_course'));
30 - add_action('wp_ajax_detach_instructor_from_course', array($this, 'detach_instructor_from_course'));
148 + /**
149 + * Frontend Dashboard
150 + */
151 + add_action( 'wp_ajax_tutor_delete_dashboard_course', array( $this, 'tutor_delete_dashboard_course' ) );
152 +
153 + /**
154 + * Gutenberg author support
155 + */
156 + add_filter( 'wp_insert_post_data', array( $this, 'tutor_add_gutenberg_author' ), 99, 2 );
157 +
158 + /**
159 + * Do Stuff for the course save from frontend
160 + */
161 + add_action( 'save_tutor_course', array( $this, 'attach_product_with_course' ), 10, 2 );
162 +
163 + /**
164 + * Enable Disable Course Details Page Feature
165 + *
166 + * @since v.1.4.8
167 + */
168 + $this->course_elements_enable_disable();
169 +
170 + /**
171 + * Check if course starting, set meta if starting
172 + *
173 + * @since v.1.4.8
174 + */
175 + add_action( 'tutor_lesson_load_before', array( $this, 'tutor_lesson_load_before' ) );
176 +
177 + /**
178 + * Filter product in shop page
179 + *
180 + * @since v.1.4.9
181 + */
182 + $this->filter_product_in_shop_page();
183 +
184 + /**
185 + * Remove the course price if enrolled
186 + *
187 + * @since 1.5.8
188 + */
189 + add_filter( 'tutor_course_price', array( $this, 'remove_price_if_enrolled' ) );
190 +
191 + /**
192 + * Remove course complete button if course completion is strict mode
193 + *
194 + * @since v.1.6.1
195 + */
196 + add_filter( 'tutor_course/single/complete_form', array( $this, 'tutor_lms_hide_course_complete_btn' ) );
197 + add_filter( 'get_gradebook_generate_form_html', array( $this, 'get_generate_greadbook' ) );
198 +
199 + /**
200 + * Add social share content in header
201 + *
202 + * @since v.1.6.3
203 + */
204 + add_action( 'wp_head', array( $this, 'social_share_content' ) );
205 +
206 + /**
207 + * Delete course data after deleted course
208 + *
209 + * @since v.1.6.6
210 + */
211 + add_action( 'deleted_post', array( new CourseModel(), 'delete_course_data' ) );
212 +
213 + /**
214 + * Delete course data after deleted course
215 + *
216 + * @since v.1.8.2
217 + */
218 + add_action( 'before_delete_post', array( $this, 'delete_associated_enrollment' ) );
219 +
220 + /**
221 + * Show only own uploads in media library if user is instructor
222 + *
223 + * @since v1.8.9
224 + */
225 + add_filter( 'posts_where', array( $this, 'restrict_media' ) );
226 +
227 + /**
228 + * Restrict new enrol/purchase button if course member limit reached
229 + *
230 + * @since v1.9.0
231 + */
232 + add_filter( 'tutor_course_restrict_new_entry', array( $this, 'restrict_new_student_entry' ) );
233 +
234 + /**
235 + * Reset course progress on retake
236 + *
237 + * @since v1.9.5
238 + */
239 + add_action( 'wp_ajax_tutor_reset_course_progress', array( $this, 'tutor_reset_course_progress' ) );
240 +
241 + /**
242 + * Popup for review
243 + *
244 + * @since v1.9.7
245 + */
246 + add_action( 'wp_footer', array( $this, 'popup_review_form' ) );
247 + add_action( 'wp_ajax_tutor_clear_review_popup_data', array( $this, 'clear_review_popup_data' ) );
248 +
249 + /**
250 + * Do enroll after login if guest take enroll attempt
251 + *
252 + * @since 1.9.8
253 + */
254 + add_action( 'tutor_do_enroll_after_login_if_attempt', array( $this, 'enroll_after_login_if_attempt' ), 10, 2 );
255 +
256 + add_action( 'wp_ajax_tutor_update_course_content_order', array( $this, 'ajax_update_course_content_order' ) );
257 +
258 + add_action( 'wp_ajax_tutor_get_wc_product', array( $this, 'get_wc_product' ) );
259 + add_action( 'wp_ajax_tutor_get_wc_products', array( $this, 'get_wc_products' ) );
260 +
261 + add_action( 'wp_ajax_tutor_course_enrollment', array( $this, 'course_enrollment' ) );
262 +
263 + /**
264 + * After trash a course redirect to course list page
265 + *
266 + * @since 2.1.7
267 + */
268 + add_action( 'trashed_post', __CLASS__ . '::redirect_to_course_list_page' );
269 +
270 + add_filter( 'tutor_enroll_required_login_class', array( $this, 'add_enroll_required_login_class' ) );
271 +
272 + /**
273 + * Remove wp trash button if instructor settings is disabled
274 + *
275 + * @since 2.7.3
276 + */
277 + add_action( 'tutor_option_save_after', array( $this, 'disable_course_trash_instructor' ) );
278 +
279 + /**
280 + * New course builder
281 + *
282 + * @since 3.0.0
283 + */
284 + add_action( 'template_redirect', array( $this, 'load_course_builder' ) );
285 + add_action( 'tutor_before_course_builder_load', array( $this, 'enqueue_course_builder_assets' ) );
286 + add_filter( 'tutor_localize_data', array( $this, 'localize_course_builder_data' ) );
287 +
288 + /**
289 + * Ajax list
290 + *
291 + * @since 3.0.0
292 + */
293 + add_action( 'wp_ajax_tutor_create_new_draft_course', array( $this, 'ajax_create_new_draft_course' ) );
294 + add_action( 'wp_ajax_tutor_course_list', array( $this, 'ajax_course_list' ) );
295 + add_action( 'wp_ajax_tutor_create_course', array( $this, 'ajax_create_course' ) );
296 + add_action( 'wp_ajax_tutor_course_details', array( $this, 'ajax_course_details' ) );
297 + add_action( 'wp_ajax_tutor_course_contents', array( $this, 'ajax_course_contents' ) );
298 + add_action( 'wp_ajax_tutor_update_course', array( $this, 'ajax_update_course' ) );
299 + add_action( 'wp_ajax_tutor_unlink_page_builder', array( $this, 'ajax_unlink_page_builder' ) );
300 +
301 + add_filter( 'tutor_user_list_access', array( $this, 'user_list_access_for_instructor' ) );
302 + add_filter( 'tutor_user_list_args', array( $this, 'user_list_args_for_instructor' ) );
303 +
304 + add_filter( 'template_include', array( $this, 'handle_password_protected' ) );
305 + add_action( 'login_form_postpass', array( $this, 'handle_password_submit' ) );
306 + add_filter( 'post_password_required', array( $this, 'bypass_password_for_enrolled' ), 10, 2 );
307 + add_filter( 'the_preview', array( $this, 'handle_schedule_courses' ) );
308 +
309 + add_action( 'tutor_course_action_btn', array( $this, 'render_course_action_btn' ) );
310 + add_action( 'wp_ajax_tutor_complete_course', array( $this, 'ajax_tutor_complete_course' ) );
31 311 }
312 +
32 313 /**
33 - * Registering metabox
314 + * Handle schedule courses preview for instructors.
315 + *
316 + * @since 3.5.0
317 + *
318 + * @param \WP_Post $content the preview post content.
319 + *
320 + * @return \WP_Post
34 321 */
35 - public function register_meta_box(){
36 - $coursePostType = tutor()->course_post_type;
322 + public function handle_schedule_courses( $content ) {
323 + global $wp_query;
324 + $course_coming_soon_enabled = (int) get_post_meta( $content->ID, '_tutor_course_enable_coming_soon', true );
325 + $is_instructor = tutor_utils()->is_instructor_of_this_course( get_current_user_id(), $content->ID, true );
326 + if ( ! CourseModel::get_post_types( $content ) || current_user_can( 'administrator' ) || $is_instructor || $course_coming_soon_enabled ) {
327 + return $content;
328 + }
37 329
38 - add_meta_box( 'tutor-course-topics', __( 'Course Builder', 'tutor' ), array($this, 'course_meta_box'), $coursePostType );
39 - add_meta_box( 'tutor-course-additional-data', __( 'Additional Data', 'tutor' ), array($this, 'course_additional_data_meta_box'), $coursePostType );
40 - add_meta_box( 'tutor-course-videos', __( 'Video', 'tutor' ), array($this, 'video_metabox'), $coursePostType );
41 - add_meta_box( 'tutor-instructors', __( 'Instructors', 'tutor' ), array($this, 'instructors_metabox'), $coursePostType );
42 - add_meta_box( 'tutor-announcements', __( 'Announcements', 'tutor' ), array($this, 'announcements_metabox'), $coursePostType );
330 + $wp_query->set_404();
331 + status_header( 404 );
332 + nocache_headers();
333 + return $content;
43 334 }
44 - public function course_meta_box(){
45 - include tutor()->path.'views/metabox/course-topics.php';
335 +
336 + /**
337 + * Handle password protected course and bundle form submission.
338 + *
339 + * @since 3.2.1
340 + *
341 + * @return void
342 + */
343 + public function handle_password_submit() {
344 + if ( Input::has( 'post_password' ) && Input::has( 'course_id' ) ) {
345 + $course_id = Input::post( 'course_id', 0, Input::TYPE_NUMERIC );
346 + $password_required = post_password_required( $course_id );
347 + if ( $password_required ) {
348 + set_transient( 'tutor_post_password_error', __( 'Invalid password', 'tutor' ) );
349 + }
350 + }
46 351 }
47 - public function course_additional_data_meta_box(){
48 - include tutor()->path.'views/metabox/course-additional-data.php';
352 +
353 + /**
354 + * Bypass password protection for enrolled users.
355 + *
356 + * @since 4.0.0
357 + *
358 + * @param bool $required Whether the password is required.
359 + * @param WP_Post $post The post object.
360 + *
361 + * @return bool false if the current user is enrolled, original value otherwise.
362 + */
363 + public function bypass_password_for_enrolled( $required, $post ) {
364 + if ( ! $required ) {
365 + return $required;
366 + }
367 +
368 + $post_types = array( tutor()->course_post_type, tutor()->bundle_post_type );
369 + if ( in_array( $post->post_type, $post_types, true ) && EnrollmentModel::is_enrolled( $post->ID ) ) {
370 + return false;
371 + }
372 +
373 + return $required;
49 374 }
50 - public function video_metabox(){
51 - include tutor()->path.'views/metabox/video-metabox.php';
375 +
376 + /**
377 + * Handle password protected course/bundle.
378 + *
379 + * @since 3.0.0
380 + *
381 + * @param string $template template path.
382 + *
383 + * @return string template path.
384 + */
385 + public function handle_password_protected( $template ) {
386 + if ( is_single() ) {
387 + $current_post_type = get_post_type( get_the_ID() );
388 + $post_types = array( tutor()->course_post_type, tutor()->bundle_post_type );
389 + if ( in_array( $current_post_type, $post_types, true ) && post_password_required() ) {
390 + remove_all_filters( 'template_include' );
391 + return tutor()->path . '/templates/single/password-protected.php';
392 + }
393 + }
394 +
395 + return $template;
52 396 }
53 397
54 - public function announcements_metabox(){
55 - include tutor()->path.'views/metabox/announcements-metabox.php';
398 + /**
399 + * Remove move to trash button on WordPress editor for instructor.
400 + *
401 + * @since 2.7.3
402 + *
403 + * @return void
404 + */
405 + public function disable_course_trash_instructor() {
406 + $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
407 + $role = get_role( tutor()->instructor_role );
408 + if ( ! $can_trash_post ) {
409 + $role->remove_cap( 'delete_tutor_courses' );
410 + $role->remove_cap( 'delete_tutor_course' );
411 + } else {
412 + $role->add_cap( 'delete_tutor_courses' );
413 + $role->add_cap( 'delete_tutor_course' );
414 + }
56 415 }
57 416
58 - public function instructors_metabox(){
59 - include tutor()->path.'views/metabox/instructors-metabox.php';
417 + /**
418 + * Check if the video source type is valid
419 + *
420 + * @since 3.0.0
421 + *
422 + * @param string $source_type source type.
423 + *
424 + * @return boolean
425 + */
426 + private function is_valid_video_source_type( string $source_type ): bool {
427 + $supported_types = tutor_utils()->get_option( 'supported_video_sources', array() );
428 + if ( is_string( $supported_types ) ) {
429 + $supported_types = array( $supported_types );
430 + }
431 +
432 + return in_array( $source_type, $supported_types, true );
60 433 }
61 434
62 435 /**
63 - * @param $post_ID
436 + * Get course selling options.
64 437 *
65 - * Insert Topic and attached it with Course
438 + * @since 3.0.0
439 + *
440 + * @return array
66 441 */
67 - public function save_course_meta($post_ID, $post){
68 - global $wpdb;
442 + public static function get_selling_options() {
443 + return array(
444 + self::SELLING_OPTION_ONE_TIME,
445 + self::SELLING_OPTION_SUBSCRIPTION,
446 + self::SELLING_OPTION_BOTH,
447 + self::SELLING_OPTION_MEMBERSHIP,
448 + self::SELLING_OPTION_ALL,
449 + );
450 + }
451 +
452 + /**
453 + * Get course selling option
454 + *
455 + * @since 3.0.0
456 + *
457 + * @param int $course_id course id.
458 + *
459 + * @return string
460 + */
461 + public static function get_selling_option( $course_id ) {
462 + return get_post_meta( $course_id, self::COURSE_SELLING_OPTION_META, true );
463 + }
464 +
465 + /**
466 + * Validate video source
467 + *
468 + * @since 3.0.0
469 + *
470 + * @param array $params array of params.
471 + * @param array $errors array of errors.
472 + *
473 + * @return void
474 + */
475 + public function validate_video_source( $params, &$errors ) {
476 + if ( isset( $params['video'] ) ) {
477 + $video_source_type = isset( $params['video']['source'] ) ? $params['video']['source'] : '';
478 + if ( tutor_is_rest() ) {
479 + $video_source_type = isset( $params['video']['source_type'] ) ? $params['video']['source_type'] : '';
480 + }
481 +
482 + if ( '' === $video_source_type ) {
483 + $errors['video_source'] = __( 'Video source is required', 'tutor' );
484 + } elseif ( ! $this->is_valid_video_source_type( $video_source_type ) ) {
485 + $errors['video_source'] = __( 'Invalid video source', 'tutor' );
486 + }
487 + }
488 + }
489 +
490 + /**
491 + * Validate scheduled courses
492 + *
493 + * @since 3.3.0
494 + *
495 + * @param array $params array of params.
496 + * @param array $errors array of errors.
497 + *
498 + * @return void
499 + */
500 + public function validate_scheduled_course( $params, &$errors ) {
501 + if ( isset( $params['post_status'] ) && isset( $params['course_settings'] ) ) {
502 + if ( 'future' !== $params['post_status'] ) {
503 + return;
504 + }
505 +
506 + $course_settings = $params['course_settings'];
507 +
508 + if ( $course_settings['course_enrollment_period'] && 'no' === $course_settings['course_enrollment_period'] ) {
509 + return;
510 + }
511 +
512 + if ( isset( $course_settings['enrollment_starts_at'] ) && ! empty( $course_settings['enrollment_starts_at'] ) ) {
513 + $enrollment_start = strtotime( $course_settings['enrollment_starts_at'] );
514 + $scheduled_date = strtotime( $params['post_date_gmt'] );
515 +
516 + if ( $enrollment_start < $scheduled_date ) {
517 + $errors['scheduled_course'] = __( 'The enrollment start date cannot be earlier than the course start date', 'tutor' );
518 + }
519 + }
520 + }
521 + }
522 +
523 + /**
524 + * Prepare course categories & tags
525 + *
526 + * @since 3.0.0
527 + *
528 + * @param array $params post params.
529 + * @param array $errors array of errors.
530 + *
531 + * @return void
532 + */
533 + public function prepare_course_cats_tags( &$params, &$errors ) {
534 + if ( isset( $params['course_categories'] ) ) {
535 + if ( ! is_array( $params['course_categories'] ) || empty( $params['course_categories'] ) ) {
536 + $errors['course_categories'] = __( 'Invalid course categories', 'tutor' );
537 + } else {
538 + $params['course_categories'] = $params['course_categories'];
539 + }
540 + }
541 +
542 + if ( isset( $params['course_tags'] ) ) {
543 + if ( ! is_array( $params['course_tags'] ) || empty( $params['course_tags'] ) ) {
544 + $errors['course_tags'] = __( 'Invalid course tags', 'tutor' );
545 + } else {
546 + $params['course_tags'] = $params['course_tags'];
547 + }
548 + }
549 + }
550 +
551 + /**
552 + * Setup course categories and tags.
553 + *
554 + * @since 3.0.0
555 + *
556 + * @param int $post_id Post ID.
557 + * @param array $params Array of params.
558 + *
559 + * @return void
560 + */
561 + public function setup_course_categories_tags( $post_id, $params ) {
562 + if ( isset( $params['course_categories'] ) && is_array( $params['course_categories'] ) ) {
563 + $valid_category_ids = ValidationHelper::validate_term_ids(
564 + $params['course_categories'],
565 + CourseModel::COURSE_CATEGORY
566 + );
567 + wp_set_object_terms( $post_id, $valid_category_ids, CourseModel::COURSE_CATEGORY );
568 + } else {
569 + wp_set_object_terms( $post_id, array(), CourseModel::COURSE_CATEGORY );
570 + }
571 +
572 + if ( isset( $params['course_tags'] ) && is_array( $params['course_tags'] ) ) {
573 + $valid_tag_ids = ValidationHelper::validate_term_ids(
574 + $params['course_tags'],
575 + CourseModel::COURSE_TAG
576 + );
577 + wp_set_object_terms( $post_id, $valid_tag_ids, CourseModel::COURSE_TAG );
578 + } else {
579 + wp_set_object_terms( $post_id, array(), CourseModel::COURSE_TAG );
580 + }
581 + }
582 +
583 + /**
584 + * Validate price for course create
585 + *
586 + * @since 3.0.0
587 + *
588 + * @param array $params array of params.
589 + * @param array $errors array of errors.
590 + *
591 + * @return void
592 + */
593 + public function validate_price( $params, &$errors ) {
594 + if ( isset( $params['pricing'] ) ) {
595 + $type = $params['pricing']['type'] ?? '';
596 +
597 + if ( '' === $type || ! in_array( $type, array( self::PRICE_TYPE_FREE, self::PRICE_TYPE_PAID ), true ) ) {
598 + $errors['pricing'] = __( 'Invalid price type', 'tutor' );
599 + }
600 +
601 + if ( self::PRICE_TYPE_PAID === $type ) {
602 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
603 + if ( 'wc' === $monetize_by ) {
604 + $product_id = (int) isset( $params['pricing']['product_id'] ) ? $params['pricing']['product_id'] : 0;
605 + // $product_id = 0 then new WC product will be created.
606 + if ( $product_id ) {
607 + $product = wc_get_product( $product_id );
608 + if ( is_a( $product, 'WC_Product' ) ) {
609 + $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
610 + if ( $is_linked_with_course ) {
611 + $errors['pricing'] = __( 'Product already linked with course', 'tutor' );
612 + }
613 + }
614 + }
615 + }
616 + }
617 + }
618 + }
619 +
620 + /**
621 + * Validate price
622 + *
623 + * @since 3.0.0
624 + *
625 + * @param array $params array of params.
626 + * @param array $errors array of errors.
627 + * @param int $course_id course id.
628 + *
629 + * @return void
630 + */
631 + public function validate_price_for_update( $params, &$errors, $course_id ) {
632 + if ( isset( $params['pricing'] ) ) {
633 + $type = $params['pricing']['type'] ?? '';
634 +
635 + if ( '' === $type || ! in_array( $type, array( self::PRICE_TYPE_FREE, self::PRICE_TYPE_PAID, self::PRICE_TYPE_SUBSCRIPTION ), true ) ) {
636 + $errors['pricing'] = __( 'Invalid price type', 'tutor' );
637 + }
638 +
639 + if ( self::PRICE_TYPE_PAID === $type ) {
640 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
641 + if ( 'wc' === $monetize_by ) {
642 + $course_product_id = tutor_utils()->get_course_product_id( $course_id );
643 + $product_id = isset( $params['pricing']['product_id'] ) ? (int) $params['pricing']['product_id'] : 0;
644 +
645 + if ( $product_id ) {
646 + $product = wc_get_product( $product_id );
647 + if ( is_a( $product, 'WC_Product' ) ) {
648 + if ( $course_product_id !== $product_id ) {
649 + $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
650 + if ( $is_linked_with_course ) {
651 + $errors['pricing'] = __( 'Product already linked with course', 'tutor' );
652 + }
653 + }
654 + } else {
655 + $errors['pricing'] = __( 'Invalid product', 'tutor' );
656 + }
657 + } else {
658 + /**
659 + * If user does not select WC product
660 + * Then automatic WC product will be create name with course title.
661 + */
662 + if ( ! isset( $params['course_price'] ) || ! floatval( $params['course_price'] ) ) {
663 + $errors['pricing'] = __( 'Price is required', 'tutor' );
664 + }
665 + }
666 + }
667 + }
668 + }
669 + }
670 +
671 + /**
672 + * Prepare course meta data for update
673 + *
674 + * @param array $params params.
675 + *
676 + * @return void
677 + */
678 + public function prepare_create_post_meta( $params ) {
679 + $additional_content = isset( $params['additional_content'] ) ? $params['additional_content'] : array();
680 +
681 + $course_benefits = isset( $additional_content['course_benefits'] ) ? $additional_content['course_benefits'] : '';
682 +
683 + $course_target_audience = isset( $additional_content['course_target_audience'] ) ? $additional_content['course_target_audience'] : '';
684 +
685 + $course_duration = isset( $additional_content['course_duration'] ) ? array(
686 + 'hours' => $additional_content['course_duration']['hours'] ?? '',
687 + 'minutes' => $additional_content['course_duration']['minutes'] ?? '',
688 + ) : array();
689 +
690 + $course_materials = isset( $additional_content['course_material_includes'] ) ? $additional_content['course_material_includes'] : '';
691 +
692 + $course_requirements = isset( $additional_content['course_requirements'] ) ? $additional_content['course_requirements'] : '';
693 +
694 + $pricing = isset( $params['pricing'] ) ? array(
695 + 'type' => $params['pricing']['type'] ?? self::PRICE_TYPE_FREE,
696 + 'product_id' => (int) $params['pricing']['product_id'] ?? -1,
697 + ) : array(
698 + 'type' => self::PRICE_TYPE_FREE,
699 + 'product_id' => -1,
700 + );
701 +
702 + // Setup global $_POST array.
703 + $_POST['_tutor_course_additional_data_edit'] = true;
704 +
705 + $_POST['tutor_course_price_type'] = $pricing['type'];
706 + $_POST['course_duration'] = $course_duration;
707 + $_POST['tutor_course_price_type'] = $pricing['type'];
708 + $_POST['_tutor_course_product_id'] = $pricing['product_id'];
709 + $_POST['_tutor_course_level'] = $params['course_level'];
710 + $_POST['course_benefits'] = $course_benefits;
711 + $_POST['course_requirements'] = $course_requirements;
712 + $_POST['course_target_audience'] = $course_target_audience;
713 + $_POST['course_material_includes'] = $course_materials;
714 +
715 + if ( isset( $params['enable_qna'] ) && 'yes' === $params['enable_qna'] ) {
716 + $_POST['_tutor_enable_qa'] = 'yes';
717 + }
718 +
719 + if ( isset( $params['_tutor_is_public_course'] ) && 'yes' === $params['_tutor_is_public_course'] ) {
720 + $_POST['_tutor_is_public_course'] = 'yes';
721 + }
722 +
723 + // Set course price.
724 + if ( -1 !== $pricing['product_id'] ) {
725 + $product = wc_get_product( $pricing['product_id'] );
726 + if ( is_a( $product, 'WC_Product' ) ) {
727 + $regular_price = $product->get_regular_price();
728 + $sale_price = $product->get_sale_price();
729 +
730 + $_POST['course_price'] = $regular_price;
731 + $_POST['course_sale_price'] = $sale_price;
732 + }
733 + }
734 + }
735 +
736 + /**
737 + * Prepare course meta data for update
738 + *
739 + * @since 3.0.0
740 + *
741 + * @param array $params params.
742 + *
743 + * @throws \Exception Throw new exception.
744 + *
745 + * @return mixed
746 + */
747 + public function prepare_update_post_meta( $params ) {
748 + $post_id = (int) $params['ID'];
749 +
750 + $additional_content = isset( $params['additional_content'] ) ? $params['additional_content'] : array();
751 +
752 + if ( ! empty( $additional_content ) ) {
753 +
754 + $course_benefits = isset( $additional_content['course_benefits'] ) ? $additional_content['course_benefits'] : '';
755 +
756 + $course_target_audience = isset( $additional_content['course_target_audience'] ) ? $additional_content['course_target_audience'] : '';
757 +
758 + $course_duration = isset( $additional_content['course_duration'] ) ? array(
759 + 'hours' => $additional_content['course_duration']['hours'] ?? '',
760 + 'minutes' => $additional_content['course_duration']['minutes'] ?? '',
761 + ) : array();
762 +
763 + $course_materials = isset( $additional_content['course_material_includes'] ) ? $additional_content['course_material_includes'] : '';
764 +
765 + $course_requirements = isset( $additional_content['course_requirements'] ) ? $additional_content['course_requirements'] : '';
766 +
767 + if ( '' !== $course_benefits ) {
768 + update_post_meta( $post_id, '_tutor_course_benefits', $course_benefits );
769 + }
770 +
771 + if ( '' !== $course_requirements ) {
772 + update_post_meta( $post_id, '_tutor_course_requirements', $course_requirements );
773 + }
774 +
775 + if ( '' !== $course_target_audience ) {
776 + update_post_meta( $post_id, '_tutor_course_target_audience', $course_target_audience );
777 + }
778 +
779 + if ( '' !== $course_materials ) {
780 + update_post_meta( $post_id, '_tutor_course_material_includes', $course_materials );
781 + }
782 +
783 + if ( ! empty( $course_duration ) ) {
784 + update_post_meta( $post_id, '_course_duration', $course_duration );
785 + }
786 + }
787 +
788 + if ( isset( $params['pricing'] ) && ! empty( $params['pricing'] ) ) {
789 + try {
790 + if ( isset( $params['pricing']['type'] ) ) {
791 + update_post_meta( $post_id, self::COURSE_PRICE_TYPE_META, $params['pricing']['type'] );
792 + }
793 + } catch ( \Throwable $th ) {
794 + throw new \Exception( $th->getMessage() );
795 + }
796 + }
797 +
798 + update_post_meta( $post_id, '_tutor_enable_qa', $params['enable_qna'] ?? 'yes' );
799 + update_post_meta( $post_id, '_tutor_is_public_course', $params['is_public_course'] ?? 'no' );
800 + update_post_meta( $post_id, '_tutor_course_level', $params['course_level'] );
801 +
69 802 /**
70 - * Insert Topic
803 + * Save tax collection settings
804 + *
805 + * @since 3.7.0
71 806 */
72 - if ( ! empty($_POST['topic_title'])) {
73 - $topic_title = sanitize_text_field( $_POST['topic_title'] );
74 - $topic_summery = wp_kses_post( $_POST['topic_summery'] );
807 + if ( isset( $params['tax_on_single'] ) ) {
808 + update_post_meta( $post_id, self::TAX_ON_SINGLE_META, $params['tax_on_single'] );
809 + }
75 810
76 - $post_arr = array(
77 - 'post_type' => 'topics',
78 - 'post_title' => $topic_title,
79 - 'post_content' => $topic_summery,
80 - 'post_status' => 'publish',
81 - 'post_author' => get_current_user_id(),
82 - 'post_parent' => $post_ID,
811 + if ( isset( $params['tax_on_subscription'] ) ) {
812 + update_post_meta( $post_id, self::TAX_ON_SUBSCRIPTION_META, $params['tax_on_subscription'] );
813 + }
814 +
815 + do_action( 'tutor_after_prepare_update_post_meta', $post_id, $params );
816 + }
817 +
818 + /**
819 + * Prepare course settings meta
820 + *
821 + * @since 3.0.0
822 + *
823 + * @param array $params params.
824 + *
825 + * @return void
826 + */
827 + public function prepare_course_settings( $params ) {
828 + if ( isset( $params['course_settings'] ) ) {
829 + $_POST['_tutor_course_settings'] = $params['course_settings'];
830 + }
831 + }
832 +
833 + /**
834 + * Check access before course builder ajax request.
835 + *
836 + * @since 3.0.0
837 + *
838 + * @param int $course_id course id.
839 + *
840 + * @return void
841 + */
842 + public function check_access( $course_id = null ) {
843 + $has_access = false;
844 +
845 + if ( $course_id ) {
846 + $has_access = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
847 + } else {
848 + $has_access = User::is_admin() || User::is_instructor();
849 + }
850 +
851 + if ( ! $has_access ) {
852 + $this->json_response(
853 + tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ),
854 + null,
855 + HttpHelper::STATUS_UNAUTHORIZED
83 856 );
84 - wp_insert_post( $post_arr );
85 857 }
858 + }
86 859
87 - //Course Duration
88 - if ( ! empty($_POST['course_duration'])){
89 - $video = tutor_utils()->sanitize_array($_POST['course_duration']);
90 - update_post_meta($post_ID, '_course_duration', $video);
860 + /**
861 + * Validate request inputs.
862 + *
863 + * @param array $params input params.
864 + * @param array $exclude exclude key from rules.
865 + *
866 + * @return object
867 + */
868 + public function validate_inputs( $params, $exclude = array() ) {
869 + $status_str = implode( ',', CourseModel::get_status_list() );
870 + $rules = array(
871 + 'course_id' => 'required|numeric',
872 + 'post_title' => 'required',
873 + 'post_author' => 'user_exists',
874 + 'post_status' => "required|match_string:{$status_str}",
875 + 'enable_qna' => 'if_input|match_string:yes,no',
876 + 'is_public_course' => 'if_input|match_string:yes,no',
877 + );
878 +
879 + foreach ( $exclude as $key ) {
880 + if ( isset( $rules[ $key ] ) ) {
881 + unset( $rules[ $key ] );
882 + }
91 883 }
92 884
93 - if ( ! empty($_POST['course_level'])){
94 - $course_level = sanitize_text_field($_POST['course_level']);
95 - update_post_meta($post_ID, '_tutor_course_level', $course_level);
885 + return ValidationHelper::validate( $rules, $params );
886 + }
887 +
888 + /**
889 + * Create new draft course
890 + *
891 + * @since 3.0.0
892 + *
893 + * @return void JSON response
894 + */
895 + public function ajax_create_new_draft_course() {
896 + tutor_utils()->check_nonce();
897 +
898 + $this->check_access();
899 +
900 + $course_id = wp_insert_post(
901 + array(
902 + 'post_title' => __( 'New Course', 'tutor' ),
903 + 'post_type' => tutor()->course_post_type,
904 + 'post_status' => 'draft',
905 + 'post_name' => 'new-course',
906 + )
907 + );
908 +
909 + if ( is_wp_error( $course_id ) ) {
910 + $this->json_response( $course_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
96 911 }
97 912
98 - if ( ! empty($_POST['course_benefits'])){
99 - $course_benefits = wp_kses_post($_POST['course_benefits']);
100 - update_post_meta($post_ID, '_tutor_course_benefits', $course_benefits);
913 + update_post_meta( $course_id, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_FREE );
914 +
915 + $link = admin_url( 'admin.php?page=create-course' );
916 + if ( tutor()->has_pro && Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
917 + $link = tutor_utils()->tutor_dashboard_url( 'create-course' );
101 918 }
102 919
103 - if ( ! empty($_POST['course_requirements'])){
104 - $requirements = wp_kses_post($_POST['course_requirements']);
105 - update_post_meta($post_ID, '_tutor_course_requirements', $requirements);
920 + $link = add_query_arg( array( 'course_id' => $course_id ), $link );
921 +
922 + do_action( 'tutor_draft_course_created', $course_id );
923 +
924 + $this->json_response(
925 + __( 'Draft course created', 'tutor' ),
926 + $link,
927 + HttpHelper::STATUS_CREATED
928 + );
929 + }
930 +
931 + /**
932 + * Get course list
933 + *
934 + * @since 3.0.0
935 + *
936 + * @since 3.2.0 Refactor the arguments & response as per new design.
937 + *
938 + * @return void
939 + */
940 + public function ajax_course_list() {
941 + tutor_utils()->check_nonce();
942 + $this->check_access();
943 +
944 + $limit = Input::post( 'limit', 10, Input::TYPE_INT );
945 + $offset = Input::post( 'offset', 0, Input::TYPE_INT );
946 + $search_term = '';
947 + $post_status = Input::post( 'post_status', null );
948 +
949 + $filter = json_decode( wp_unslash( $_POST['filter'] ) ); //phpcs:ignore --sanitized already
950 + if ( ! empty( $filter ) && property_exists( $filter, 'search' ) ) {
951 + $search_term = Input::sanitize( $filter->search );
106 952 }
107 953
108 - if ( ! empty($_POST['course_target_audience'])){
109 - $target_audience = wp_kses_post($_POST['course_target_audience']);
110 - update_post_meta($post_ID, '_tutor_course_target_audience', $target_audience);
954 + $args = array(
955 + 'post_status' => is_null( $post_status ) ? 'publish' : $post_status,
956 + 'posts_per_page' => $limit,
957 + 'offset' => $offset,
958 + 's' => $search_term,
959 + );
960 +
961 + $exclude = Input::post( 'exclude', array(), Input::TYPE_ARRAY );
962 + if ( count( $exclude ) ) {
963 + $exclude = array_filter( $exclude, fn( $id ) => is_numeric( $id ) && $id > 0 );
964 + $args['post__not_in'] = $exclude;
111 965 }
112 966
113 - if ( ! empty($_POST['course_material_includes'])){
114 - $material_includes = wp_kses_post($_POST['course_material_includes']);
115 - update_post_meta($post_ID, '_tutor_course_material_includes', $material_includes);
967 + $courses = CourseModel::get_courses_by_args( $args );
968 +
969 + $response = array(
970 + 'results' => array(),
971 + 'total_items' => 0,
972 + );
973 +
974 + $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
975 +
976 + if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
977 + $courses = $courses->get_posts();
978 + foreach ( $courses as $course ) {
979 + $response['results'][] = self::get_mini_info( $course );
980 + }
116 981 }
982 +
983 + $this->json_response(
984 + __( 'Course list retrieved successfully!', 'tutor' ),
985 + $response
986 + );
987 + }
988 +
989 + /**
990 + * Create course by ajax request.
991 + *
992 + * @since 3.0.0
993 + *
994 + * @return void
995 + */
996 + public function ajax_create_course() {
997 + tutor_utils()->check_nonce();
998 +
999 + $this->check_access();
1000 +
1001 + $params = Input::sanitize_array(
1002 + //phpcs:ignore WordPress.Security.NonceVerification.Missing
1003 + $_POST,
1004 + array(
1005 + 'post_content' => 'wp_kses_post',
1006 + 'course_material_includes' => 'sanitize_textarea_field',
1007 + )
1008 + );
1009 +
1010 + $params['post_type'] = tutor()->course_post_type;
1011 +
1012 + // Validate inputs.
1013 + $errors = array();
1014 + $validation = $this->validate_inputs( $params, array( 'course_id' ) );
1015 + if ( ! $validation->success ) {
1016 + $errors = $validation->errors;
1017 + }
1018 +
1019 + if ( User::is_instructor() ) {
1020 + $params['post_author'] = get_current_user_id();
1021 + }
1022 +
1023 + // Validate video source if user set video.
1024 + $this->validate_video_source( $params, $errors );
1025 +
1026 + // Validate WC product.
1027 + $this->validate_price( $params, $errors );
1028 +
1029 + // Set course categories and tags.
1030 + $this->prepare_course_cats_tags( $params, $errors );
1031 + $this->setup_course_price( $params );
1032 +
1033 + if ( ! empty( $errors ) ) {
1034 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1035 + }
1036 +
1037 + $this->prepare_course_settings( $params );
1038 +
1039 + try {
1040 + $this->prepare_create_post_meta( $params );
1041 + } catch ( \Exception $e ) {
1042 + $this->json_response( $e->getMessage(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
1043 + }
1044 +
1045 + $course_id = wp_insert_post( $params );
1046 + if ( is_wp_error( $course_id ) ) {
1047 + $this->json_response( $course_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
1048 + }
1049 +
1050 + // Set course cats & tags.
1051 + $this->setup_course_categories_tags( $course_id, $params );
1052 +
1053 + // Update course thumb.
1054 + if ( isset( $params['thumbnail_id'] ) ) {
1055 + set_post_thumbnail( $course_id, $params['thumbnail_id'] );
1056 + }
1057 +
1058 + $this->json_response(
1059 + __( 'Course created successfully', 'tutor' ),
1060 + $course_id,
1061 + HttpHelper::STATUS_CREATED
1062 + );
1063 + }
1064 +
1065 + /**
1066 + * Setup course price
1067 + *
1068 + * @since 3.0.0
1069 + *
1070 + * @param array $params params.
1071 + *
1072 + * @return void
1073 + */
1074 + public function setup_course_price( $params ) {
1075 + if ( isset( $params['pricing'] )
1076 + && isset( $params['pricing']['product_id'] )
1077 + && is_numeric( $params['pricing']['product_id'] ) ) {
1078 + $_POST['_tutor_course_product_id'] = $params['pricing']['product_id'];
1079 + }
1080 + }
1081 +
1082 + /**
1083 + * Update course by ajax request.
1084 + *
1085 + * @since 3.0.0
1086 + *
1087 + * @return void
1088 + */
1089 + public function ajax_update_course() {
1090 + tutor_utils()->check_nonce();
1091 +
1092 + $params = Input::sanitize_array(
1093 + //phpcs:ignore WordPress.Security.NonceVerification.Missing
1094 + wp_slash( $_POST ),
1095 + array(
1096 + 'post_content' => 'wp_kses_post',
1097 + 'course_benefits' => 'sanitize_textarea_field',
1098 + 'course_target_audience' => 'sanitize_textarea_field',
1099 + 'course_material_includes' => 'sanitize_textarea_field',
1100 + 'course_requirements' => 'sanitize_textarea_field',
1101 + )
1102 + );
1103 +
1104 + $course_id = (int) $params['course_id'];
1105 + if ( ! $course_id ) {
1106 + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1107 + }
1108 +
1109 + $this->check_access( $course_id );
1110 +
1111 + $errors = array();
1112 + $validation = $this->validate_inputs( $params );
1113 + if ( ! $validation->success ) {
1114 + $errors = $validation->errors;
1115 + }
1116 +
1117 + // Validate video source if user set video.
1118 + $this->validate_video_source( $params, $errors );
1119 +
1120 + // Validate WC product.
1121 + $this->validate_price_for_update( $params, $errors, $course_id );
1122 +
1123 + // Set course categories and tags.
1124 + $this->prepare_course_cats_tags( $params, $errors );
1125 +
1126 + $this->prepare_course_settings( $params );
1127 +
1128 + // Validate scheduled courses.
1129 + $this->validate_scheduled_course( $params, $errors );
1130 +
1131 + $this->setup_course_price( $params );
1132 +
1133 + if ( ! empty( $errors ) ) {
1134 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1135 + }
1136 +
117 1137 /**
118 - * Sorting Topics and lesson
1138 + * Can trash a course when user is admin or option `instructor_can_delete_course` is turned on.
119 1139 */
120 - if ( ! empty($_POST['tutor_topics_lessons_sorting'])){
121 - $new_order = sanitize_text_field(stripslashes($_POST['tutor_topics_lessons_sorting']));
122 - $order = json_decode($new_order, true);
1140 + if ( CourseModel::STATUS_TRASH === $params['post_status'] ) {
1141 + if ( User::is_admin() || tutor_utils()->get_option( 'instructor_can_delete_course', false ) ) {
1142 + $params['post_status'] = CourseModel::STATUS_TRASH;
1143 + } else {
1144 + unset( $params['post_status'] );
1145 + }
1146 + }
123 1147
124 - if (is_array($order) && count($order)){
125 - $i = 0;
126 - foreach ($order as $topic ){
127 - $i++;
128 - $wpdb->update(
129 - $wpdb->posts,
130 - array('menu_order' => $i),
131 - array('ID' => $topic['topic_id'])
132 - );
1148 + /**
1149 + * Can publish a course when user is admin or option `instructor_can_publish_course` is turned on.
1150 + * If instructor_can_publish_course is turned off then course status will be pending.
1151 + */
1152 + if ( CourseModel::STATUS_PUBLISH === $params['post_status'] ) {
1153 + $is_instructor_allowed_to_publish = (bool) tutor_utils()->get_option( 'instructor_can_publish_course', false );
1154 + if ( ! User::is_admin() && ! $is_instructor_allowed_to_publish ) {
1155 + $params['post_status'] = CourseModel::STATUS_PENDING;
1156 + }
1157 + }
133 1158
134 - /**
135 - * Removing All lesson with topic
136 - */
1159 + $is_error = apply_filters( 'tutor_is_error_before_course_update', false, $params );
1160 + if ( is_wp_error( $is_error ) ) {
1161 + $this->response_bad_request( $is_error->get_error_message() );
1162 + }
137 1163
138 - $wpdb->update(
139 - $wpdb->posts,
140 - array('post_parent' => 0),
141 - array('post_parent' => $topic['topic_id'])
142 - );
1164 + $params['ID'] = $course_id;
1165 + $update_id = wp_update_post( $params, true );
1166 + if ( is_wp_error( $update_id ) ) {
1167 + $this->json_response( $update_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
1168 + }
143 1169
144 - /**
145 - * Lesson Attaching with topic ID
146 - * sorting lesson
147 - */
148 - if (isset($topic['lesson_ids'])){
149 - $lesson_ids = $topic['lesson_ids'];
150 - }else{
151 - $lesson_ids = array();
152 - }
153 - if (count($lesson_ids)){
154 - foreach ($lesson_ids as $lesson_key => $lesson_id ){
155 - $wpdb->update(
156 - $wpdb->posts,
157 - array('post_parent' => $topic['topic_id'], 'menu_order' => $lesson_key),
158 - array('ID' => $lesson_id)
159 - );
1170 + $this->setup_course_categories_tags( $update_id, $params );
1171 + $this->prepare_update_post_meta( $params );
1172 +
1173 + // Update course thumb.
1174 + $thumbnail_id = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
1175 + if ( $thumbnail_id ) {
1176 + set_post_thumbnail( $update_id, $thumbnail_id );
1177 + } else {
1178 + delete_post_meta( $update_id, '_thumbnail_id' );
1179 + }
1180 +
1181 + $this->json_response(
1182 + __( 'Course updated successfully.', 'tutor' ),
1183 + $update_id,
1184 + HttpHelper::STATUS_OK
1185 + );
1186 + }
1187 +
1188 + /**
1189 + * Unlink page builder from editor.
1190 + *
1191 + * @since 3.0.0
1192 + *
1193 + * @return void
1194 + */
1195 + public function ajax_unlink_page_builder() {
1196 + tutor_utils()->check_nonce();
1197 +
1198 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1199 + if ( ! $course_id ) {
1200 + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1201 + }
1202 +
1203 + $this->check_access( $course_id );
1204 +
1205 + $builder = Input::post( 'builder' );
1206 + if ( 'elementor' === $builder ) {
1207 + delete_post_meta( $course_id, '_elementor_edit_mode' );
1208 + } elseif ( 'droip' === $builder ) {
1209 + delete_post_meta( $course_id, 'droip_editor_mode' );
1210 + } elseif ( 'divi' === $builder ) {
1211 + $old_post_content = get_post_meta( $course_id, '_et_pb_old_content', true );
1212 + $course = get_post( $course_id );
1213 + $course->post_content = $old_post_content;
1214 + $result = wp_update_post( $course );
1215 +
1216 + if ( $result && ! is_wp_error( $result ) ) {
1217 + update_post_meta( $course_id, '_et_pb_use_builder', 'off' );
1218 + update_post_meta( $course_id, '_et_pb_old_content', '' );
1219 + delete_post_meta( $course_id, '_et_dynamic_cached_shortcodes' );
1220 + delete_post_meta( $course_id, '_et_dynamic_cached_attributes' );
1221 + delete_post_meta( $course_id, '_et_builder_module_features_cache' );
1222 + }
1223 + }
1224 +
1225 + $this->json_response(
1226 + __( 'Builder unlinked successfully.', 'tutor' ),
1227 + $course_id,
1228 + HttpHelper::STATUS_OK
1229 + );
1230 + }
1231 +
1232 + /**
1233 + * Get all course contents by course id.
1234 + *
1235 + * @since 3.0.0
1236 + *
1237 + * @param int $course_id course id.
1238 + *
1239 + * @return array
1240 + */
1241 + public function get_course_contents( $course_id ) {
1242 + $data = array();
1243 + $topics = tutor_utils()->get_topics( $course_id );
1244 +
1245 + if ( $topics->have_posts() ) {
1246 + foreach ( $topics->get_posts() as $post ) {
1247 + $current_topic = array(
1248 + 'id' => $post->ID,
1249 + 'title' => $post->post_title,
1250 + 'summary' => $post->post_content,
1251 + 'contents' => array(),
1252 + );
1253 +
1254 + $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 );
1255 +
1256 + if ( $topic_contents->have_posts() ) {
1257 + foreach ( $topic_contents->get_posts() as $post ) {
1258 + if ( tutor()->quiz_post_type === $post->post_type ) {
1259 + $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1260 + $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
160 1261 }
1262 +
1263 + array_push( $current_topic['contents'], $post );
161 1264 }
162 1265 }
1266 +
1267 + $current_topic = apply_filters( 'tutor_filter_course_content', $current_topic );
1268 +
1269 + array_push( $data, $current_topic );
163 1270 }
164 1271 }
165 1272
1273 + return $data;
1274 + }
166 1275
167 - //Video
168 - if ( ! empty($_POST['video']['source'])){
169 - $video = tutor_utils()->sanitize_array($_POST['video']);
170 - update_post_meta($post_ID, '_video', $video);
1276 + /**
1277 + * Get course contents
1278 + *
1279 + * @since 3.0.0
1280 + *
1281 + * @return void
1282 + */
1283 + public function ajax_course_contents() {
1284 + tutor_utils()->check_nonce();
1285 +
1286 + $errors = array();
1287 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1288 +
1289 + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1290 + $errors['course_id'] = __( 'Invalid course id', 'tutor' );
171 1291 }
172 1292
1293 + $this->check_access( $course_id );
1294 +
1295 + if ( ! empty( $errors ) ) {
1296 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1297 + }
1298 +
1299 + $contents = $this->get_course_contents( $course_id );
1300 +
1301 + $this->json_response(
1302 + __( 'Course contents fetched successfully', 'tutor' ),
1303 + $contents
1304 + );
1305 + }
1306 +
1307 + /**
1308 + * Get course details by ID
1309 + *
1310 + * @since 3.0.0
1311 + *
1312 + * @return void
1313 + */
1314 + public function ajax_course_details() {
1315 + tutor_utils()->check_nonce();
1316 +
1317 + $errors = array();
1318 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1319 +
1320 + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1321 + $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1322 + }
1323 +
1324 + $this->check_access( $course_id );
1325 +
1326 + if ( ! empty( $errors ) ) {
1327 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1328 + }
1329 +
1330 + $price_type = tutor_utils()->price_type( $course_id );
1331 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1332 +
1333 + $product_name = '';
1334 + $price = 0;
1335 + $sale_price = 0;
1336 + $product_id = tutor_utils()->get_course_product_id( $course_id );
1337 +
1338 + if ( WooCommerce::MONETIZE_BY === $monetize_by ) {
1339 + $product = wc_get_product( $product_id );
1340 + if ( $product ) {
1341 + $product_name = $product->get_name();
1342 + $price = $product->get_regular_price();
1343 + $sale_price = $product->get_sale_price();
1344 + }
1345 + }
1346 +
1347 + if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
1348 + $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1349 + $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1350 + }
1351 +
1352 + $course_pricing = array(
1353 + 'type' => $price_type,
1354 + 'product_id' => $product_id,
1355 + 'product_name' => $product_name,
1356 + 'price' => $price,
1357 + 'sale_price' => $sale_price,
1358 + );
1359 +
1360 + $video_intro = get_post_meta( $course_id, '_video', true );
1361 + if ( $video_intro ) {
1362 + $source = $video_intro['source'] ?? '';
1363 + if ( 'html5' === $source ) {
1364 + $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1365 + $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1366 + $video['poster_url'] = $poster_url;
1367 + $video['source_html5'] = $source_html5;
1368 + }
1369 + }
1370 +
1371 + $course = get_post( $course_id, ARRAY_A );
1372 + if ( $course ) {
1373 + $course['post_name'] = urldecode( $course['post_name'] );
1374 + }
1375 +
1376 + $editors = tutor_utils()->get_editor_list( $course_id );
1377 +
1378 + $data = array(
1379 + 'editors' => array_values( $editors ),
1380 + 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1381 + 'preview_link' => get_preview_post_link( $course_id ),
1382 + 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1383 + 'course_categories' => wp_get_post_terms( $course_id, CourseModel::COURSE_CATEGORY ),
1384 + 'course_tags' => wp_get_post_terms( $course_id, CourseModel::COURSE_TAG ),
1385 + 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1386 + 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
1387 +
1388 + 'enable_qna' => get_post_meta( $course_id, '_tutor_enable_qa', true ),
1389 + 'is_public_course' => get_post_meta( $course_id, '_tutor_is_public_course', true ),
1390 + 'course_level' => get_post_meta( $course_id, '_tutor_course_level', true ),
1391 + 'video' => $video_intro,
1392 + 'course_duration' => get_post_meta( $course_id, '_course_duration', true ),
1393 + 'course_benefits' => get_post_meta( $course_id, '_tutor_course_benefits', true ),
1394 + 'course_requirements' => get_post_meta( $course_id, '_tutor_course_requirements', true ),
1395 + 'course_target_audience' => get_post_meta( $course_id, '_tutor_course_target_audience', true ),
1396 + 'course_material_includes' => get_post_meta( $course_id, '_tutor_course_material_includes', true ),
1397 + 'monetize_by' => $monetize_by,
1398 + 'course_pricing' => $course_pricing,
1399 + 'course_settings' => get_post_meta( $course_id, '_tutor_course_settings', true ),
1400 + 'step_completion_status' => array(
1401 + 'basic' => true,
1402 + 'curriculum' => false,
1403 + 'additional' => false,
1404 + 'certificate' => false,
1405 + ),
1406 + );
1407 +
1408 + $tax_on_single = get_post_meta( $course_id, self::TAX_ON_SINGLE_META, true );
1409 + $tax_on_subscription = get_post_meta( $course_id, self::TAX_ON_SUBSCRIPTION_META, true );
1410 +
1411 + $data['tax_collection'] = array(
1412 + 'tax_on_single' => '' === $tax_on_single ? '1' : $tax_on_single,
1413 + 'tax_on_subscription' => '' === $tax_on_subscription ? '1' : $tax_on_subscription,
1414 + );
1415 +
1416 + $data = apply_filters( 'tutor_course_details_response', array_merge( $course, $data ) );
1417 +
1418 + $this->json_response( __( 'Data retrieved successfully!', 'tutor' ), $data );
1419 + }
1420 +
1421 + /**
1422 + * Load course builder.
1423 + *
1424 + * @since 3.0.0
1425 + *
1426 + * @return void
1427 + */
1428 + public function load_course_builder() {
1429 + global $pagenow;
1430 +
1431 + $has_pro = tutor()->has_pro;
1432 + $has_access_role = User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) );
1433 +
1434 + $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1435 + $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1436 + $backend_edit = $backend_builder && $course_id;
1437 +
1438 + $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1439 + $frontend_edit = $is_frontend_builder && $course_id;
1440 +
1441 + if ( $has_access_role && ( $backend_edit || ( $has_pro && $frontend_edit ) ) ) {
1442 + $post_type = get_post_type( $course_id );
1443 + $can_edit_course = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
1444 +
1445 + if ( tutor()->course_post_type === $post_type && ( User::is_admin() || $can_edit_course ) ) {
1446 + /**
1447 + * Edit trash course behavior
1448 + *
1449 + * @since 3.0.0
1450 + */
1451 + if ( CourseModel::STATUS_TRASH === get_post_status( $course_id ) ) {
1452 + $message = User::is_admin()
1453 + ? __( 'You cannot edit this course because it is in the Trash. Please restore it and try again', 'tutor' )
1454 + : tutor_utils()->error_message();
1455 + wp_die( esc_html( $message ) );
1456 + }
1457 +
1458 + $this->load_course_builder_view();
1459 + }
1460 + }
1461 + }
1462 +
1463 + /**
1464 + * Enqueue course builder assets like CSS, JS
1465 + *
1466 + * @since 3.0.0
1467 + *
1468 + * @return void
1469 + */
1470 + public function enqueue_course_builder_assets() {
1471 + // Fix: function print_emoji_styles is deprecated since version 6.4.0!
1472 + remove_action( 'wp_print_styles', 'print_emoji_styles' );
1473 + remove_action( 'wp_head', 'wp_admin_bar_header' );
1474 + add_action( 'wp_head', 'wp_enqueue_admin_bar_header_styles' );
1475 +
1476 + do_action( 'tutor_course_builder_before_wp_editor_load' );
1477 + wp_enqueue_script( 'wp-tinymce' );
1478 + wp_enqueue_script( 'mce-view' );
1479 + wp_enqueue_editor();
1480 +
1481 + wp_enqueue_media();
1482 + wp_enqueue_script( 'tutor-course-builder', tutor()->url . 'assets/js/tutor-course-builder.js', array( 'wp-date', 'wp-i18n', 'wp-element', 'wp-api' ), TUTOR_VERSION, true );
1483 + wp_set_script_translations( 'tutor-course-builder', 'tutor', tutor()->path . 'languages/' );
1484 +
1485 + wp_localize_script(
1486 + 'mce-view',
1487 + 'mceViewL10n',
1488 + array(
1489 + 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1490 + )
1491 + );
1492 + }
1493 +
1494 + /**
1495 + * Localize custom course builder data for _tutorobject.
1496 + *
1497 + * @since 3.3.1
1498 + *
1499 + * @param array $data the localized data.
1500 + *
1501 + * @return array
1502 + */
1503 + public function localize_course_builder_data( $data ) {
1504 + global $pagenow;
1505 +
1506 + $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1507 + $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1508 + $backend_edit = $backend_builder && $course_id;
1509 +
1510 + $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1511 + $frontend_edit = $is_frontend_builder && $course_id;
1512 +
1513 + if ( ! $backend_edit && ! $frontend_edit ) {
1514 + return $data;
1515 + }
1516 +
173 1517 /**
174 - * Adding author to instructor automatically
1518 + * Prepare course builder data.
175 1519 */
1520 + $default_data = ( new Assets( false ) )->get_default_localized_data();
176 1521
177 - $author_id = $post->post_author;
178 - $attached = (int) $wpdb->get_var(" SELECT COUNT(umeta_id) FROM {$wpdb->usermeta} WHERE user_id = {$author_id} AND meta_key = '_tutor_instructor_course_id' AND meta_value = {$post_ID} ");
179 - if ( ! $attached){
180 - add_user_meta($author_id, '_tutor_instructor_course_id', $post_ID);
1522 + if ( isset( $default_data['current_user']['data']['id'] ) ) {
1523 + $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1524 + $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
181 1525 }
182 1526
183 - //Announcements
184 - $announcement_title = tutor_utils()->avalue_dot('announcements.title', $_POST );
185 - if ( ! empty($announcement_title)){
186 - $title = sanitize_text_field(tutor_utils()->avalue_dot('announcements.title', $_POST ));
187 - $content = wp_kses_post(tutor_utils()->avalue_dot('announcements.content', $_POST ));
1527 + /**
1528 + * Localized only options to protect sensitive info like API keys.
1529 + */
1530 + $required_options = array(
1531 + 'learning_mode',
1532 + 'monetize_by',
1533 + 'enable_course_marketplace',
1534 + 'course_permalink_base',
1535 + 'supported_video_sources',
1536 + 'enrollment_expiry_enabled',
1537 + 'enable_q_and_a_on_course',
1538 + 'instructor_can_delete_course',
1539 + 'chatgpt_enable',
1540 + 'hide_admin_bar_for_users',
1541 + 'enable_redirect_on_course_publish_from_frontend',
1542 + 'instructor_can_publish_course',
1543 + 'instructor_can_change_course_author',
1544 + 'instructor_can_manage_co_instructors',
1545 + );
188 1546
189 - $post_arr = array(
190 - 'post_type' => 'tutor_announcements',
191 - 'post_title' => $title,
192 - 'post_content' => $content,
193 - 'post_status' => 'publish',
194 - 'post_author' => get_current_user_id(),
195 - 'post_parent' => $post_ID,
1547 + $full_settings = get_option( 'tutor_option', array() );
1548 + $settings = Options_V2::get_only( $required_options );
1549 + $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1550 + $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1551 + $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1552 +
1553 + $settings['enable_tax'] = Tax::get_setting( 'enable_tax', true );
1554 + $settings['is_tax_included_in_price'] = Tax::is_tax_included_in_price();
1555 + $settings['enable_individual_tax_control'] = Tax::get_setting( 'enable_individual_tax_control' );
1556 +
1557 + $new_data = array( 'settings' => $settings );
1558 +
1559 + $data = array_merge( $default_data, $new_data );
1560 +
1561 + /**
1562 + * Course builder dashboard URL based on role and settings.
1563 + */
1564 + $dashboard_url = tutor_utils()->tutor_dashboard_url();
1565 + if ( User::is_admin() ) {
1566 + $dashboard_url = get_admin_url();
1567 + }
1568 +
1569 + /**
1570 + * EDD product list
1571 + */
1572 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1573 + if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1574 + $data['edd_products'] = tutor_utils()->get_edd_products();
1575 + }
1576 +
1577 + $difficulty_levels = array();
1578 + foreach ( tutor_utils()->course_levels() as $value => $label ) {
1579 + $difficulty_levels[] = array(
1580 + 'label' => $label,
1581 + 'value' => $value,
196 1582 );
197 - wp_insert_post( $post_arr );
198 1583 }
1584 +
1585 + $supported_video_sources = array();
1586 + $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1587 +
1588 + foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1589 + if ( in_array( $value, $saved_video_source_list, true ) ) {
1590 + $supported_video_sources[] = array(
1591 + 'label' => $label,
1592 + 'value' => $value,
1593 + );
1594 + }
1595 + }
1596 +
1597 + $data['dashboard_url'] = $dashboard_url;
1598 + $data['backend_course_list_url'] = get_admin_url( null, 'admin.php?page=tutor' );
1599 + $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1600 + $data['timezones'] = tutor_global_timezone_lists();
1601 + $data['difficulty_levels'] = $difficulty_levels;
1602 + $data['supported_video_sources'] = $supported_video_sources;
1603 + $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1604 +
1605 + if ( 'en_US' !== $data['local'] ) {
1606 + $data['course_builder_basic_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-basic', $data['local'] );
1607 + $data['course_builder_curriculum_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-curriculum', $data['local'] );
1608 + $data['course_builder_additional_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-additional', $data['local'] );
1609 + }
1610 +
1611 + $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1612 +
1613 + return $data;
199 1614 }
200 1615
201 1616 /**
202 - * Tutor add course topic
1617 + * Load view for course builder.
1618 + *
1619 + * @since 3.0.0
1620 + *
1621 + * @return void
203 1622 */
204 - public function tutor_add_course_topic(){
205 - if (empty($_POST['topic_title'])) {
206 - wp_send_json_error();
1623 + public function load_course_builder_view() {
1624 + /**
1625 + * Hide admin menu and footer.
1626 + *
1627 + * @since 3.3.0
1628 + */
1629 + echo '<style>
1630 + #adminmenumain, #wpfooter, .notice, #tutor-page-wrap { display: none !important; }
1631 + #wpcontent { margin: 0 !important; padding: 0 !important; }
1632 + #wpbody-content { padding-bottom: 0px !important; float: none; }
1633 + </style>';
1634 +
1635 + do_action( 'tutor_before_course_builder_load' );
1636 + include_once tutor()->path . 'views/pages/course-builder.php';
1637 + do_action( 'tutor_after_course_builder_load' );
1638 + }
1639 +
1640 + /**
1641 + * Add enroll require login class
1642 + *
1643 + * @since 2.6.0
1644 + *
1645 + * @param string $class_name css class name.
1646 + *
1647 + * @return string
1648 + */
1649 + public function add_enroll_required_login_class( $class_name ) {
1650 + $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1651 + if ( ! $enabled_tutor_login ) {
1652 + return '';
207 1653 }
208 - $course_id = (int) tutor_utils()->avalue_dot('tutor_topic_course_ID', $_POST);
209 - $next_topic_order_id = tutor_utils()->get_next_topic_order_id($course_id);
210 1654
211 - $topic_title = sanitize_text_field( $_POST['topic_title'] );
212 - $topic_summery = wp_kses_post( $_POST['topic_summery'] );
1655 + return $class_name;
1656 + }
213 1657
214 - $post_arr = array(
215 - 'post_type' => 'topics',
216 - 'post_title' => $topic_title,
217 - 'post_content' => $topic_summery,
218 - 'post_status' => 'publish',
219 - 'post_author' => get_current_user_id(),
220 - 'post_parent' => $course_id,
221 - 'menu_order' => $next_topic_order_id,
1658 + /**
1659 + * Get list of WC products.
1660 + *
1661 + * @since 2.5.0
1662 + * @since 3.0.0 exclude_linked_products, course_id are added.
1663 + *
1664 + * @return void
1665 + */
1666 + public function get_wc_products() {
1667 + $exclude = array();
1668 + $exclude_linked_products = Input::has( 'exclude_linked_products' );
1669 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1670 +
1671 + if ( $exclude_linked_products ) {
1672 + $exclude = tutor_utils()->get_linked_product_ids();
1673 + }
1674 +
1675 + if ( $course_id ) {
1676 + $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1677 + if ( $linked_product_id ) {
1678 + $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1679 + }
1680 + }
1681 +
1682 + $exclude = array_unique( $exclude );
1683 +
1684 + $this->json_response(
1685 + __( 'Products retrieved successfully!', 'tutor' ),
1686 + tutor_utils()->get_wc_products_db( $exclude ),
1687 + HttpHelper::STATUS_OK
222 1688 );
223 - $current_topic_id = wp_insert_post( $post_arr );
1689 + }
224 1690
225 - ob_start();
226 - include tutor()->path.'views/metabox/course-contents.php';
227 - $course_contents = ob_get_clean();
1691 + /**
1692 + * Get course associate WC product info by Ajax request
1693 + *
1694 + * @since 2.0.7
1695 + *
1696 + * @return void
1697 + */
1698 + public function get_wc_product() {
1699 + tutor_utils()->checking_nonce();
1700 + $product_id = Input::post( 'product_id' );
1701 + $product = wc_get_product( $product_id );
1702 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
228 1703
229 - wp_send_json_success(array('course_contents' => $course_contents));
1704 + $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1705 +
1706 + /**
1707 + * If selected product is already linked with
1708 + * a course & it is not the current course the
1709 + * return error
1710 + *
1711 + * @since 2.1.0
1712 + */
1713 + if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1714 + wp_send_json_error(
1715 + __( 'One product can not be added to multiple course!', 'tutor' )
1716 + );
1717 + }
1718 +
1719 + if ( $product ) {
1720 + $data = array(
1721 + 'name' => $product->get_name(),
1722 + 'regular_price' => $product->get_regular_price(),
1723 + 'sale_price' => $product->get_sale_price(),
1724 + );
1725 + wp_send_json_success( $data );
1726 + } else {
1727 + wp_send_json_error( __( 'Product not found', 'tutor' ) );
1728 + }
230 1729 }
231 1730
232 1731 /**
233 - * Update the topic
1732 + * Update course content order
1733 + *
1734 + * @since 1.0.0
1735 + * @since 3.9.9 Check if user can manage course before updating order.
1736 + *
1737 + * @return void
234 1738 */
235 - public function tutor_update_topic(){
236 - $topic_id = (int) sanitize_text_field($_POST['topic_id']);
237 - $topic_title = sanitize_text_field($_POST['topic_title']);
238 - $topic_summery = wp_kses_post($_POST['topic_summery']);
1739 + public function ajax_update_course_content_order() {
1740 + tutor_utils()->checking_nonce();
239 1741
240 - $topic_attr = array(
241 - 'ID' => $topic_id,
242 - 'post_title' => $topic_title,
243 - 'post_content' => $topic_summery,
244 - );
245 - wp_update_post( $topic_attr );
1742 + $sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
1743 + $sorting_order = json_decode( $sorting_order, true ) ?? array();
246 1744
247 - wp_send_json_success(array('msg' => __('Topic has been updated', 'tutor') ));
1745 + if ( ! tutor_utils()->count( $sorting_order ) ) {
1746 + wp_send_json_error( __( 'Sorting order is required', 'tutor' ) );
1747 + }
1748 +
1749 + $topic_id = (int) isset( $sorting_order[0], $sorting_order[0]['topic_id'] ) ? $sorting_order[0]['topic_id'] : 0;
1750 + $course_id = wp_get_post_parent_id( $topic_id );
1751 +
1752 + if ( ! $topic_id || ! $course_id ) {
1753 + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
1754 + }
1755 +
1756 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ! User::is_admin() ) {
1757 + wp_send_json_error( tutor_utils()->error_message() );
1758 + }
1759 +
1760 + if ( Input::has( 'content_parent' ) ) {
1761 + $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1762 + $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1763 + $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1764 +
1765 + // Update the parent topic id of the content.
1766 + global $wpdb;
1767 + $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1768 + }
1769 +
1770 + // Save course content order.
1771 + $this->save_course_content_order( $sorting_order );
1772 +
1773 + wp_send_json_success();
248 1774 }
249 1775
250 -
251 1776 /**
252 - * @param $columns
1777 + * Restrict new student entry
253 1778 *
1779 + * @since 1.0.0
1780 + * @param mixed $content content.
1781 + *
254 1782 * @return mixed
255 - *
256 - * Add Lesson column
257 1783 */
1784 + public function restrict_new_student_entry( $content ) {
258 1785
259 - public function add_column($columns){
260 - $date_col = $columns['date'];
261 - unset($columns['date']);
262 - $columns['lessons'] = __('Lessons', 'tutor');
263 - $columns['students'] = __('Students', 'tutor');
264 - $columns['price'] = __('Price', 'tutor');
265 - $columns['date'] = $date_col;
1786 + if ( ! tutor_utils()->is_course_fully_booked() ) {
1787 + // No restriction if not fully booked.
1788 + return $content;
1789 + }
266 1790
267 - return $columns;
1791 + return '<div class="list-item-booking booking-full tutor-d-flex tutor-align-center"><div class="booking-progress tutor-d-flex"><span class="tutor-mr-8 tutor-color-warning tutor-icon-circle-info"></span></div><div class="tutor-fs-7 tutor-fw-medium">' .
1792 + __( 'Fully Booked', 'tutor' )
1793 + . '</div></div>';
268 1794 }
269 1795
270 1796 /**
271 - * @param $column
272 - * @param $post_id
1797 + * Restrict media
273 1798 *
1799 + * @since 1.0.0
1800 + *
1801 + * @param string $where where clause.
1802 + *
1803 + * @return string
274 1804 */
275 - public function custom_lesson_column($column, $post_id ){
276 - if ($column === 'lessons'){
277 - echo tutor_utils()->get_lesson_count_by_course($post_id);
1805 + public function restrict_media( $where ) {
1806 + $action = Input::post( 'action' );
1807 + if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1808 + if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1809 + $where .= ' AND post_author=' . get_current_user_id();
1810 + }
278 1811 }
279 1812
280 - if ($column === 'students'){
281 - echo tutor_utils()->count_enrolled_users_by_course($post_id);
1813 + return $where;
1814 + }
1815 +
1816 + /**
1817 + * Save course content order
1818 + *
1819 + * @since 1.0.0
1820 + * @since 3.9.8 param $order added.
1821 + *
1822 + * @param array $sort_order the lesson and topic order array.
1823 + *
1824 + * @return void
1825 + */
1826 + private function save_course_content_order( $sort_order = array() ) {
1827 + global $wpdb;
1828 +
1829 + if ( ! tutor_utils()->count( $sort_order ) ) {
1830 + return;
282 1831 }
283 1832
284 - if ($column === 'price'){
285 - $price = tutor_utils()->get_course_price($post_id);
1833 + $i = 0;
1834 + foreach ( $sort_order as $topic ) {
1835 + ++$i;
1836 + $wpdb->update(
1837 + $wpdb->posts,
1838 + array( 'menu_order' => $i ),
1839 + array( 'ID' => $topic['topic_id'] )
1840 + );
286 1841
287 - if ($price){
288 - if (function_exists('wc_price')){
289 - echo '<span class="tutor-label-success">'.wc_price($price).'</span>';
290 - }else{
291 - echo '<span class="tutor-label-success">'.$price.'</span>';
1842 + /**
1843 + * Removing All lesson with topic
1844 + */
1845 + $wpdb->update(
1846 + $wpdb->posts,
1847 + array( 'post_parent' => 0 ),
1848 + array( 'post_parent' => $topic['topic_id'] )
1849 + );
1850 +
1851 + /**
1852 + * Lesson Attaching with topic ID
1853 + * Sorting lesson
1854 + */
1855 + if ( isset( $topic['lesson_ids'] ) ) {
1856 + $lesson_ids = $topic['lesson_ids'];
1857 + } else {
1858 + $lesson_ids = array();
1859 + }
1860 + if ( count( $lesson_ids ) ) {
1861 + foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1862 + $wpdb->update(
1863 + $wpdb->posts,
1864 + array(
1865 + 'post_parent' => $topic['topic_id'],
1866 + 'menu_order' => $lesson_key,
1867 + ),
1868 + array( 'ID' => $lesson_id )
1869 + );
292 1870 }
293 - }else{
294 - echo 'free';
295 1871 }
296 1872 }
297 1873 }
298 1874
1875 + /**
1876 + * Insert Topic and attached it with Course
1877 + *
1878 + * @since 1.0.0
1879 + *
1880 + * @param integer $post_ID post ID.
1881 + * @param object $post post object.
1882 + *
1883 + * @return void
1884 + */
1885 + public function save_course_meta( $post_ID, $post ) {
1886 + global $wpdb;
299 1887
300 - public function tutor_delete_topic(){
301 - if (!isset($_GET[tutor()->nonce]) || !wp_verify_nonce($_GET[tutor()->nonce], tutor()->nonce_action)) {
302 - exit();
1888 + do_action( 'tutor_save_course', $post_ID, $post );
1889 +
1890 + /**
1891 + * Save course price type
1892 + */
1893 + $price_type = Input::post( 'tutor_course_price_type' );
1894 + if ( $price_type ) {
1895 + update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
303 1896 }
304 - if ( ! isset($_GET['topic_id'])){
305 - exit();
1897 +
1898 + //phpcs:disable WordPress.Security.NonceVerification.Missing
1899 + // Course Duration.
1900 + if ( ! empty( $_POST['course_duration'] ) ) {
1901 + $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1902 + update_post_meta( $post_ID, '_course_duration', $video );
306 1903 }
307 1904
308 - global $wpdb;
1905 + if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1906 + $course_level = Input::post( '_tutor_course_level' );
1907 + update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1908 + }
309 1909
310 - $topic_id = (int) sanitize_text_field($_GET['topic_id']);
311 - $wpdb->update(
312 - $wpdb->posts,
313 - array('post_parent' => 0),
314 - array('post_parent' => $topic_id)
1910 + $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1911 + if ( $additional_data_edit ) {
1912 + if ( ! empty( $_POST['course_benefits'] ) ) {
1913 + $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1914 + update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1915 + } elseif ( ! tutor_is_rest() ) {
1916 + delete_post_meta( $post_ID, '_tutor_course_benefits' );
1917 + }
1918 +
1919 + if ( ! empty( $_POST['course_requirements'] ) ) {
1920 + $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1921 + update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1922 + } elseif ( ! tutor_is_rest() ) {
1923 + delete_post_meta( $post_ID, '_tutor_course_requirements' );
1924 + }
1925 +
1926 + if ( ! empty( $_POST['course_target_audience'] ) ) {
1927 + $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1928 + update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1929 + } elseif ( ! tutor_is_rest() ) {
1930 + delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1931 + }
1932 +
1933 + if ( ! empty( $_POST['course_material_includes'] ) ) {
1934 + $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1935 + update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1936 + } elseif ( ! tutor_is_rest() ) {
1937 + delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1938 + }
1939 + //phpcs:enable WordPress.Security.NonceVerification.Missing
1940 + }
1941 +
1942 + $sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
1943 + $sorting_order = json_decode( $sorting_order, true ) ?? array();
1944 + /**
1945 + * Sorting Topics and lesson
1946 + */
1947 + $this->save_course_content_order( $sorting_order );
1948 +
1949 + // Additional data like course intro video.
1950 + if ( $additional_data_edit ) {
1951 + // Sanitize data through helper method.
1952 + $video = Input::sanitize_array(
1953 + $_POST['video'] ?? array(), //phpcs:ignore
1954 + array(
1955 + 'source_external_url' => 'esc_url',
1956 + 'source_embedded' => 'wp_kses_post',
1957 + ),
1958 + true
1959 + );
1960 + $video_source = tutor_utils()->array_get( 'source', $video );
1961 + if ( -1 !== $video_source ) {
1962 + update_post_meta( $post_ID, '_video', $video );
1963 + } elseif ( ! tutor_is_rest() ) {
1964 + delete_post_meta( $post_ID, '_video' );
1965 + }
1966 + }
1967 +
1968 + /**
1969 + * Adding author to instructor automatically
1970 + */
1971 +
1972 + // Override post author id.
1973 + $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1974 + $attached = (int) $wpdb->get_var(
1975 + $wpdb->prepare(
1976 + "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1977 + WHERE user_id = %d
1978 + AND meta_key = '_tutor_instructor_course_id'
1979 + AND meta_value = %d ",
1980 + $author_id,
1981 + $post_ID
1982 + )
315 1983 );
316 1984
317 - $wpdb->delete(
318 - $wpdb->postmeta,
319 - array('post_id' => $topic_id)
1985 + if ( ! $attached ) {
1986 + add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1987 + }
1988 +
1989 + /**
1990 + * Disable question and answer for this course
1991 + *
1992 + * @since 1.7.0
1993 + */
1994 + if ( $additional_data_edit ) {
1995 + foreach ( $this->additional_meta as $key ) {
1996 + //phpcs:ignore WordPress.Security.NonceVerification.Missing
1997 + update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1998 + }
1999 + }
2000 +
2001 + do_action( 'tutor_save_course_after', $post_ID, $post );
2002 + }
2003 +
2004 + /**
2005 + * Save course topic
2006 + *
2007 + * @since 1.0.0
2008 + * @since 3.0.0 response and input name updated.
2009 + *
2010 + * @return void
2011 + */
2012 + public function tutor_save_topic() {
2013 + tutor_utils()->check_nonce();
2014 +
2015 + $is_update = false;
2016 + $errors = array();
2017 + $topic_title = Input::post( 'title' );
2018 +
2019 + if ( empty( $topic_title ) ) {
2020 + $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
2021 + $this->json_response(
2022 + __( 'Invalid inputs', 'tutor' ),
2023 + $errors,
2024 + HttpHelper::STATUS_UNPROCESSABLE_ENTITY
2025 + );
2026 + }
2027 +
2028 + // Gather parameters.
2029 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2030 + $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2031 + $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
2032 +
2033 + $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
2034 +
2035 + // Validate if user can manage the topic.
2036 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
2037 + wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2038 + }
2039 +
2040 + // Create payload to create/update the topic.
2041 + $post_arr = array(
2042 + 'post_type' => 'topics',
2043 + 'post_title' => $topic_title,
2044 + 'post_content' => $topic_summary,
2045 + 'post_status' => 'publish',
2046 + 'post_author' => get_current_user_id(),
2047 + 'post_parent' => $course_id,
2048 + 'menu_order' => $next_topic_order_id,
320 2049 );
321 2050
322 - wp_delete_post($topic_id);
323 - wp_safe_redirect(wp_get_referer());
2051 + if ( $topic_id ) {
2052 + $is_update = true;
2053 + $post_arr['ID'] = $topic_id;
2054 + }
2055 +
2056 + $current_topic_id = wp_insert_post( $post_arr );
2057 +
2058 + if ( $is_update ) {
2059 + $this->json_response(
2060 + __( 'Topic updated successfully!', 'tutor' ),
2061 + $current_topic_id
2062 + );
2063 + } else {
2064 + $this->json_response(
2065 + __( 'Topic created successfully!', 'tutor' ),
2066 + $current_topic_id,
2067 + HttpHelper::STATUS_CREATED
2068 + );
2069 + }
324 2070 }
325 2071
326 - public function tutor_delete_announcement(){
327 - tutor_utils()->checking_nonce('get');
2072 + /**
2073 + * Delete a course topic
2074 + *
2075 + * @since 1.0.0
2076 + * @since 3.0.0 code refactor and response updated.
2077 + *
2078 + * @return void
2079 + */
2080 + public function tutor_delete_topic() {
2081 + tutor_utils()->check_nonce();
328 2082
329 - $announcement_id = (int) sanitize_text_field($_GET['topic_id']);
2083 + $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2084 + if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
2085 + $this->json_response(
2086 + tutor_utils()->error_message(),
2087 + null,
2088 + HttpHelper::STATUS_FORBIDDEN
2089 + );
2090 + }
330 2091
331 - wp_delete_post($announcement_id);
332 - wp_safe_redirect(wp_get_referer());
2092 + global $wpdb;
2093 +
2094 + // Assign course ID to orphan content IDs since the topic will be deleted.
2095 + $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
2096 + $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
2097 + foreach ( $content_ids as $content_id ) {
2098 + update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
2099 + // Actually all kind of contents.
2100 + // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
2101 + }
2102 +
2103 + // Set contents under the topic orphan.
2104 + $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
2105 +
2106 + // Then delete the topic from database.
2107 + $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
2108 + wp_delete_post( $topic_id );
2109 +
2110 + $this->json_response(
2111 + __( 'Topic deleted successfully!', 'tutor' )
2112 + );
333 2113 }
334 2114
335 - public function enroll_now(){
336 - //Checking if action comes from Enroll form
337 - if ( ! isset($_POST['tutor_course_action']) || $_POST['tutor_course_action'] !== '_tutor_course_enroll_now' || ! isset($_POST['tutor_course_id']) ){
2115 + /**
2116 + * Handle enroll now action
2117 + *
2118 + * @since 1.0.0
2119 + *
2120 + * @return void
2121 + */
2122 + public function enroll_now() {
2123 +
2124 + if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
338 2125 return;
339 2126 }
340 - //Checking Nonce
2127 +
2128 + // Checking Nonce.
341 2129 tutor_utils()->checking_nonce();
342 2130
343 2131 $user_id = get_current_user_id();
344 - if ( ! $user_id){
345 - exit(__('Please Sign In first', 'tutor'));
2132 + if ( ! $user_id ) {
2133 + exit( esc_html__( 'Please Sign In first', 'tutor' ) );
346 2134 }
347 2135
348 - $course_id = (int) sanitize_text_field($_POST['tutor_course_id']);
349 - $user_id = get_current_user_id();
2136 + $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
350 2137
351 2138 /**
352 2139 * TODO: need to check purchase information
353 2140 */
354 2141
355 - $is_purchasable = tutor_utils()->is_course_purchasable($course_id);
2142 + $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
356 2143
2144 + $course = get_post( $course_id );
2145 +
2146 + if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
2147 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
2148 + }
2149 +
357 2150 /**
358 2151 * If is is not purchasable, it's free, and enroll right now
2152 + * If purchasable, then process purchase.
359 2153 *
360 - * if purchasable, then process purchase.
361 - *
362 2154 * @since: v.1.0.0
363 2155 */
364 - if ($is_purchasable){
365 - //process purchase
2156 + if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2157 + // Process purchase.
366 2158
367 - }else{
368 - //Free enroll
369 - tutor_utils()->do_enroll($course_id);
2159 + } else {
2160 + // Free enroll.
2161 + EnrollmentModel::do_enroll( $course_id );
370 2162 }
371 2163
372 2164 $referer_url = wp_get_referer();
373 - wp_redirect($referer_url);
2165 + wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2166 + exit;
374 2167 }
375 2168
376 2169 /**
2170 + * Mark complete completed
377 2171 *
378 - * Mark complete completed
2172 + * @since 1.0.0
379 2173 *
380 - * @since v.1.0.0
2174 + * @since 3.7.1 Filter hook: tutor_user_can_complete_course added
2175 + *
2176 + * @return void
381 2177 */
382 - public function mark_course_complete(){
383 - if ( ! isset($_POST['tutor_action']) || $_POST['tutor_action'] !== 'tutor_complete_course' ){
2178 + public function mark_course_complete() {
2179 + $tutor_action = Input::post( 'tutor_action' );
2180 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2181 + if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
384 2182 return;
385 2183 }
386 - //Checking nonce
2184 +
2185 + $permalink = get_the_permalink( $course_id );
2186 +
2187 + // Checking nonce.
387 2188 tutor_utils()->checking_nonce();
388 2189
389 2190 $user_id = get_current_user_id();
390 2191
391 - //TODO: need to show view if not signed_in
392 - if ( ! $user_id){
393 - die(__('Please Sign-In', 'tutor'));
2192 + // TODO: need to show view if not signed_in.
2193 + if ( ! $user_id ) {
2194 + die( esc_html__( 'Please Sign-In', 'tutor' ) );
394 2195 }
395 2196
396 - $course_id = (int) sanitize_text_field($_POST['course_id']);
2197 + if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2198 + die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2199 + }
397 2200
398 - do_action('tutor_course_complete_before', $course_id);
399 2201 /**
400 - * Marking course completed at Comment
2202 + * Filter hook provided to restrict course completion. This is useful
2203 + * for specific cases like prerequisites. WP_Error should be returned
2204 + * from the filter value to prevent the completion.
401 2205 */
2206 + $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
402 2207
2208 + if ( is_wp_error( $can_complete ) ) {
2209 + tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2210 + } elseif ( ! $can_complete ) {
2211 + tutor_utils()->redirect_to( $permalink, __( 'You do not have permission to complete this course.', 'tutor' ), 'error' );
2212 + } else {
2213 + CourseModel::mark_course_as_completed( $course_id, $user_id );
2214 + // Set temporary identifier to show review pop up.
2215 + self::set_review_popup_data( $user_id, $course_id, $permalink );
2216 +
2217 + wp_safe_redirect( $permalink );
2218 + exit;
2219 + }
2220 + }
2221 +
2222 + /**
2223 + * Ajax course complete handler
2224 + *
2225 + * @since 4.0.0
2226 + *
2227 + * @return void
2228 + */
2229 + public function ajax_tutor_complete_course() {
2230 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2231 +
2232 + // Checking nonce.
2233 + if ( ! tutor_utils()->is_nonce_verified() ) {
2234 + $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
2235 + }
2236 +
2237 + if ( ! $course_id ) {
2238 + $this->response_bad_request( __( 'Invalid course', 'tutor' ) );
2239 + }
2240 +
2241 + $user_id = get_current_user_id();
2242 + if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2243 + $this->response_bad_request( __( 'You are not enrolled in this course', 'tutor' ) );
2244 + }
2245 +
2246 + /**
2247 + * Filter hook provided to restrict course completion. This is useful
2248 + * for specific cases like prerequisites. WP_Error should be returned
2249 + * from the filter value to prevent the completion.
2250 + */
2251 + $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2252 +
2253 + if ( is_wp_error( $can_complete ) ) {
2254 + $this->response_bad_request( $can_complete->get_error_message() );
2255 + } elseif ( ! $can_complete ) {
2256 + $this->response_bad_request( __( 'You do not have permission to complete this course.', 'tutor' ) );
2257 + } else {
2258 + CourseModel::mark_course_as_completed( $course_id, $user_id );
2259 + // Set temporary identifier to show review pop up.
2260 + self::set_review_popup_data( $user_id, $course_id );
2261 +
2262 + $this->response_success( __( 'Course completed successfully', 'tutor' ) );
2263 + }
2264 + }
2265 +
2266 + /**
2267 + * Set data for review popup.
2268 + *
2269 + * @since 2.2.5
2270 + * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2271 + *
2272 + * @param int $user_id user id.
2273 + * @param int $course_id course id.
2274 + *
2275 + * @return void
2276 + */
2277 + public static function set_review_popup_data( $user_id, $course_id ) {
2278 + if ( get_tutor_option( 'enable_course_review' ) ) {
2279 + $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2280 + if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2281 + $meta_key = User::get_review_popup_meta( $course_id );
2282 + add_user_meta( $user_id, $meta_key, $course_id, true );
2283 + }
2284 + }
2285 + }
2286 +
2287 + /**
2288 + * Popup review form on course details
2289 + *
2290 + * @since 1.0.0
2291 + * @return void
2292 + */
2293 + public function popup_review_form() {
2294 + if ( ! is_user_logged_in() ) {
2295 + return;
2296 + }
2297 +
2298 + $is_learning_area = tutor_utils()->is_learning_area();
2299 + $is_legacy_learning = tutor_utils()->is_legacy_learning_mode();
2300 + if ( $is_legacy_learning && $is_learning_area ) {
2301 + return;
2302 + }
2303 +
2304 + $course_id = get_the_ID();
2305 +
2306 + if ( $is_learning_area && ! Input::has( 'subpage' ) ) {
2307 + $course_id = wp_get_post_parent_id( wp_get_post_parent_id( get_the_ID() ) );
2308 + }
2309 +
2310 + if ( empty( $course_id ) ) {
2311 + return;
2312 + }
2313 +
2314 + $user_id = get_current_user_id();
2315 + $meta_key = User::get_review_popup_meta( $course_id );
2316 + $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2317 + if ( is_single() && $course_id === $review_course_id ) {
2318 + include tutor()->path . 'views/modal/review.php';
2319 + }
2320 + }
2321 +
2322 + /**
2323 + * Review popup data clear
2324 + *
2325 + * @since 2.4.0
2326 + *
2327 + * @return void
2328 + */
2329 + public function clear_review_popup_data() {
2330 + tutils()->checking_nonce();
2331 +
2332 + if ( is_user_logged_in() ) {
2333 + $user_id = get_current_user_id();
2334 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2335 +
2336 + if ( $course_id ) {
2337 + $meta_key = User::get_review_popup_meta( $course_id );
2338 + delete_user_meta( $user_id, $meta_key, $course_id );
2339 + }
2340 +
2341 + wp_send_json_success();
2342 + }
2343 + }
2344 +
2345 + /**
2346 + * Delete course delete from frontend dashboard
2347 + *
2348 + * @since 2.0.0
2349 + *
2350 + * @return void
2351 + */
2352 + public function tutor_delete_dashboard_course() {
2353 + tutor_utils()->checking_nonce();
2354 +
2355 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2356 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2357 + wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2358 + }
2359 +
2360 + /**
2361 + * Co-instructor can not delete a course
2362 + *
2363 + * @since 2.1.6
2364 + */
2365 + if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2366 + wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2367 + }
2368 +
2369 + // Check if user is only an instructor.
2370 + if ( ! User::is_admin() ) {
2371 + // Check if instructor can trash course.
2372 + $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2373 +
2374 + if ( ! $can_trash_post ) {
2375 + wp_send_json_error( tutor_utils()->error_message() );
2376 + }
2377 + }
2378 +
2379 + $trash_course = wp_update_post(
2380 + array(
2381 + 'ID' => $course_id,
2382 + 'post_status' => CourseModel::STATUS_TRASH,
2383 + )
2384 + );
2385 +
2386 + if ( $trash_course ) {
2387 + wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2388 + }
2389 + wp_send_json_success();
2390 + }
2391 +
2392 + /**
2393 + * Main author change from gutenberg editor
2394 + *
2395 + * @since 2.0.0
2396 + *
2397 + * @param array $data data.
2398 + * @param array $postarr post array.
2399 + *
2400 + * @return mixed
2401 + */
2402 + public function tutor_add_gutenberg_author( $data, $postarr ) {
2403 + $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2404 + $post_type = $postarr['post_type'];
2405 + $courses_post_type = tutor()->course_post_type;
2406 +
2407 + if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2408 + return $data;
2409 + }
2410 +
2411 + /**
2412 + * Only admin can change main author
2413 + */
2414 + if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2415 + global $wpdb;
2416 + $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2417 + $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2418 +
2419 + if ( $post_author > 0 ) {
2420 + $data['post_author'] = $post_author;
2421 + } else {
2422 + $data['post_author'] = get_current_user_id();
2423 + }
2424 + }
2425 +
2426 + return $data;
2427 + }
2428 +
2429 +
2430 + /**
2431 + * Attach product with course when course save from frontend or backend.
2432 + *
2433 + * @since 1.3.4
2434 + *
2435 + * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2436 + *
2437 + * @param integer $post_ID course ID.
2438 + * @param array $post_data created course post details.
2439 + *
2440 + * @return void
2441 + */
2442 + public function attach_product_with_course( $post_ID, $post_data ) {
2443 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2444 + $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2445 +
2446 + /**
2447 + * For native monetization, just return
2448 + * No need to attach anything.
2449 + */
2450 + if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2451 + return;
2452 + }
2453 +
2454 + /**
2455 + * When course moved paid to free
2456 + * Keep the product linked and return.
2457 + */
2458 + if ( -1 === $product_id ) {
2459 + return;
2460 + }
2461 +
2462 + /**
2463 + * Free user can only select product from dropdown
2464 + */
2465 + if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2466 + if ( $product_id > 0 ) {
2467 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2468 + }
2469 +
2470 + return;
2471 + }
2472 +
2473 + $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2474 + $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2475 + $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2476 +
2477 + if ( ! $course_price || $sale_price >= $course_price ) {
2478 + return;
2479 + }
2480 +
2481 + $course = get_post( $post_ID );
2482 +
2483 + update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2484 +
2485 + if ( 'wc' === $monetize_by ) {
2486 +
2487 + $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2488 +
2489 + if ( $is_update ) {
2490 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2491 +
2492 + $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2493 + $product_obj = wc_get_product( $product_id );
2494 + if ( $product_obj->is_type( 'subscription' ) ) {
2495 + update_post_meta( $product_id, '_subscription_price', $course_price );
2496 + }
2497 +
2498 + // Set course regular & sale price.
2499 + self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2500 + } else {
2501 + // Create new WC product name with course title.
2502 + $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2503 + if ( $product_id ) {
2504 + $product_obj = wc_get_product( $product_id );
2505 +
2506 + self::sync_course_with_wc_product( $post_ID, $product_id );
2507 +
2508 + // Set course regular & sale price.
2509 + self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2510 + }
2511 + }
2512 +
2513 + $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2514 + if ( $product_id && $course_post_thumbnail ) {
2515 + set_post_thumbnail( $product_id, $course_post_thumbnail );
2516 + }
2517 + } elseif ( 'edd' === $monetize_by ) {
2518 +
2519 + $is_update = false;
2520 +
2521 + if ( $attached_product_id ) {
2522 + $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2523 + if ( $edd_price ) {
2524 + $is_update = true;
2525 + }
2526 + }
2527 +
2528 + if ( $is_update ) {
2529 + // Update the product.
2530 + update_post_meta( $attached_product_id, 'edd_price', $course_price );
2531 + } else {
2532 + // Create new product.
2533 +
2534 + $post_arr = array(
2535 + 'post_type' => 'download',
2536 + 'post_title' => $course->post_title,
2537 + 'post_status' => 'publish',
2538 + 'post_author' => get_current_user_id(),
2539 + );
2540 + $download_id = wp_insert_post( $post_arr );
2541 + if ( $download_id ) {
2542 + // EDD edd_price.
2543 + update_post_meta( $download_id, 'edd_price', $course_price );
2544 +
2545 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2546 + // Mark product for EDD.
2547 + update_post_meta( $download_id, '_tutor_product', 'yes' );
2548 +
2549 + $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2550 + if ( $course_post_thumbnail ) {
2551 + set_post_thumbnail( $download_id, $course_post_thumbnail );
2552 + }
2553 + }
2554 + }
2555 + }
2556 + }
2557 +
2558 + /**
2559 + * Check if course starting
2560 + *
2561 + * @since 1.4.8
2562 + * @return void
2563 + */
2564 + public function tutor_lesson_load_before() {
2565 + $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2566 + $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2567 + if ( is_user_logged_in() ) {
2568 + $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2569 + if ( ! $completed_lessons && ! $is_course_started ) {
2570 + update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2571 + do_action( 'tutor/course/started', $course_id );
2572 + }
2573 + }
2574 + }
2575 +
2576 + /**
2577 + * Add Course level to course settings
2578 + *
2579 + * @since 1.4.8
2580 + *
2581 + * @return void
2582 + */
2583 + public function course_elements_enable_disable() {
2584 + add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2585 + add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2586 + add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2587 + add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2588 + add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2589 + add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2590 + add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2591 + }
2592 +
2593 + /**
2594 + * Enable disable course progress bar
2595 + *
2596 + * @since 1.4.8
2597 + *
2598 + * @param string $html HTML string.
2599 + * @return string
2600 + */
2601 + public function enable_disable_course_progress_bar( $html ) {
2602 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2603 + if ( $disable_option ) {
2604 + return '';
2605 + }
2606 + return $html;
2607 + }
2608 +
2609 + /**
2610 + * Enable disable material includes
2611 + *
2612 + * @since 1.4.8
2613 + *
2614 + * @param string $html HTML string.
2615 + *
2616 + * @return string
2617 + */
2618 + public function enable_disable_material_includes( $html ) {
2619 + $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2620 + if ( $disable_option ) {
2621 + return '';
2622 + }
2623 + return $html;
2624 + }
2625 +
2626 + /**
2627 + * Enable disable course content
2628 + *
2629 + * @since 1.4.8
2630 + *
2631 + * @param string $html HTML string.
2632 + *
2633 + * @return string
2634 + */
2635 + public function enable_disable_course_content( $html ) {
2636 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2637 + if ( $disable_option ) {
2638 + return '';
2639 + }
2640 + return $html;
2641 + }
2642 +
2643 + /**
2644 + * Enable disable course benefits
2645 + *
2646 + * @since 1.4.8
2647 + *
2648 + * @param string $html HTML string.
2649 + *
2650 + * @return string
2651 + */
2652 + public function enable_disable_course_benefits( $html ) {
2653 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2654 + if ( $disable_option ) {
2655 + return '';
2656 + }
2657 + return $html;
2658 + }
2659 +
2660 + /**
2661 + * Enable disable course requirements
2662 + *
2663 + * @since 1.4.8
2664 + *
2665 + * @param string $html HTML string.
2666 + *
2667 + * @return string
2668 + */
2669 + public function enable_disable_course_requirements( $html ) {
2670 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2671 + if ( $disable_option ) {
2672 + return '';
2673 + }
2674 + return $html;
2675 + }
2676 +
2677 + /**
2678 + * Enable disable course target audience
2679 + *
2680 + * @since 1.4.8
2681 + *
2682 + * @param string $html HTML string.
2683 + *
2684 + * @return string
2685 + */
2686 + public function enable_disable_course_target_audience( $html ) {
2687 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2688 + if ( $disable_option ) {
2689 + return '';
2690 + }
2691 + return $html;
2692 + }
2693 +
2694 + /**
2695 + * Enable disable course nav items
2696 + *
2697 + * @since 1.4.8
2698 + *
2699 + * @param array $items item list.
2700 + * @param integer $course_id course ID.
2701 + *
2702 + * @return array
2703 + */
2704 + public function enable_disable_course_nav_items( $items, $course_id ) {
2705 + global $wp_query, $post;
2706 + $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2707 + $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2708 + $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2709 +
2710 + // Whether Q&A enabled.
2711 + if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2712 + if ( tutor_utils()->array_get( 'questions', $items ) ) {
2713 + unset( $items['questions'] );
2714 + }
2715 + }
2716 +
2717 + // Whether announcment enabled.
2718 + if ( $disable_course_announcements ) {
2719 + if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2720 + unset( $items['announcements'] );
2721 + }
2722 + }
2723 +
2724 + // Hide review section if disabled.
2725 + if ( ! get_tutor_option( 'enable_course_review' ) ) {
2726 + unset( $items['reviews'] );
2727 + }
2728 +
2729 + // Whether enrollment require.
2730 + $is_enrolled = EnrollmentModel::is_enrolled();
2731 +
2732 + return array_filter(
2733 + $items,
2734 + function ( $item ) use ( $is_enrolled ) {
2735 + if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2736 + return $is_enrolled;
2737 + }
2738 + return true;
2739 + }
2740 + );
2741 + }
2742 +
2743 + /**
2744 + * Filter product in shop page
2745 + *
2746 + * @since 1.4.9
2747 + *
2748 + * @return void|null
2749 + */
2750 + public function filter_product_in_shop_page() {
2751 + $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2752 + if ( ! $hide_course_from_shop_page ) {
2753 + return;
2754 + }
2755 + add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2756 + add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2757 + add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2758 + }
2759 +
2760 +
2761 + /**
2762 + * Tutor product meta query
2763 + *
2764 + * @since 1.4.9
2765 + *
2766 + * @return array
2767 + */
2768 + public function tutor_product_meta_query() {
2769 + $meta_query = array(
2770 + 'key' => '_tutor_product',
2771 + 'compare' => 'NOT EXISTS',
2772 + );
2773 + return $meta_query;
2774 + }
2775 +
2776 + /**
2777 + * Filter product in woocommerce shop page
2778 + *
2779 + * @since 1.4.9
2780 + *
2781 + * @param \WP_Query $wp_query WP Query instance.
2782 + *
2783 + * @return \WP_Query
2784 + */
2785 + public function filter_woocommerce_product_query( $wp_query ) {
2786 + $product_ids = $this->get_connected_wc_product_ids();
2787 + $wp_query->set( 'post__not_in', $product_ids );
2788 + return $wp_query;
2789 + }
2790 +
2791 + /**
2792 + * Get connected woocommerce product ids for course and course bundle
2793 + *
2794 + * @since 2.7.2
2795 + *
2796 + * @return array
2797 + */
2798 + public function get_connected_wc_product_ids() {
403 2799 global $wpdb;
404 2800
405 - $date = date("Y-m-d H:i:s");
2801 + $results = $wpdb->get_results(
2802 + $wpdb->prepare(
2803 + "SELECT DISTINCT pm.meta_value product_id
2804 + FROM {$wpdb->posts} p
2805 + INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2806 + AND pm.meta_key = %s
2807 + WHERE post_type IN( 'courses','course-bundle' )",
2808 + '_tutor_course_product_id'
2809 + )
2810 + );
406 2811
407 - //Making sure that, hash is unique
408 - do{
409 - $hash = substr(md5(wp_generate_password(32).$date.$course_id.$user_id), 0, 16);
410 - $hasHash = (int) $wpdb->get_var("SELECT COUNT(comment_ID) from {$wpdb->comments} WHERE comment_agent = 'TutorLMSPlugin' AND comment_type = 'course_completed' AND comment_content = '{$hash}' ");
411 - }while($hasHash > 0);
2812 + $ids = array();
2813 + if ( is_array( $results ) && count( $results ) ) {
2814 + $ids = array_column( $results, 'product_id' );
2815 + }
412 2816
413 - $data = array(
414 - 'comment_post_ID' => $course_id,
415 - 'comment_author' => $user_id,
416 - 'comment_date' => $date,
417 - 'comment_date_gmt' => get_gmt_from_date($date),
418 - 'comment_content' => $hash, //Identification Hash
419 - 'comment_approved' => 'approved',
420 - 'comment_agent' => 'TutorLMSPlugin',
421 - 'comment_type' => 'course_completed',
422 - 'user_id' => $user_id,
2817 + return $ids;
2818 + }
2819 +
2820 + /**
2821 + * Filter product in edd downloads shortcode page
2822 + *
2823 + * @since 1.4.9
2824 + *
2825 + * @param \WP_Query $query WP Query instance.
2826 + *
2827 + * @return \WP_Query
2828 + */
2829 + public function filter_edd_downloads_query( $query ) {
2830 + $query['meta_query'][] = $this->tutor_product_meta_query();
2831 + return $query;
2832 + }
2833 +
2834 + /**
2835 + * Filter product in edd downloads archive page
2836 + *
2837 + * @since 1.4.9
2838 + *
2839 + * @param \WP_Query $wp_query WP Query instance.
2840 + *
2841 + * @return \WP_Query
2842 + */
2843 + public function filter_archive_meta_query( $wp_query ) {
2844 + if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2845 + $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2846 + }
2847 + return $wp_query;
2848 + }
2849 +
2850 + /**
2851 + * Removed course price if already enrolled at single course
2852 + *
2853 + * @since 1.5.8
2854 + *
2855 + * @param string $html HTML string.
2856 + *
2857 + * @return string
2858 + */
2859 + public function remove_price_if_enrolled( $html ) {
2860 + $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2861 +
2862 + if ( $should_removed ) {
2863 + $course_id = get_the_ID();
2864 + $enrolled = EnrollmentModel::is_enrolled( $course_id );
2865 + if ( $enrolled ) {
2866 + $html = '';
2867 + }
2868 + }
2869 + return $html;
2870 + }
2871 +
2872 + /**
2873 + * Get course completion missing requirements message.
2874 + *
2875 + * @since 4.0.0
2876 + *
2877 + * @param int $course_id Course ID.
2878 + * @param int $user_id User ID.
2879 + *
2880 + * @return string|null
2881 + */
2882 + public static function get_course_completion_restrict_msg( $course_id = 0, $user_id = 0 ) {
2883 + $course_id = tutor_utils()->get_post_id( $course_id );
2884 + $user_id = tutor_utils()->get_user_id( $user_id );
2885 +
2886 + if ( 'strict' !== tutor_utils()->get_option( 'course_completion_process' ) ) {
2887 + return null;
2888 + }
2889 +
2890 + $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
2891 + $total_lessons = tutor_utils()->get_lesson_count_by_course( $course_id );
2892 +
2893 + if ( $completed_lessons < $total_lessons ) {
2894 + return __( 'Complete all lessons to mark this course as complete', 'tutor' );
2895 + }
2896 +
2897 + $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
2898 +
2899 + $required_quiz_pass_count = 0;
2900 + $required_assignment_pass_count = 0;
2901 +
2902 + $is_assignment_addon_enabled = tutor_utils()->is_addon_enabled( 'tutor-assignments' );
2903 +
2904 + foreach ( $course_contents as $content ) {
2905 +
2906 + if ( tutor()->quiz_post_type === $content->post_type ) {
2907 + if ( ! QuizModel::is_quiz_passed( $content->ID, $user_id ) ) {
2908 + ++$required_quiz_pass_count;
2909 + }
2910 + }
2911 +
2912 + if ( $is_assignment_addon_enabled && tutor()->assignment_post_type === $content->post_type ) {
2913 + if ( ! Assignments::is_assignment_passed( $content->ID, $user_id ) ) {
2914 + ++$required_assignment_pass_count;
2915 + }
2916 + }
2917 + }
2918 +
2919 + if ( ! $required_quiz_pass_count && ! $required_assignment_pass_count ) {
2920 + return null;
2921 + }
2922 +
2923 + return self::get_course_completion_requirement_message(
2924 + $required_quiz_pass_count,
2925 + $required_assignment_pass_count
423 2926 );
2927 + }
424 2928
425 - $wpdb->insert($wpdb->comments, $data);
2929 + /**
2930 + * Build missing course completion requirements message.
2931 + *
2932 + * @since 4.0.0
2933 + *
2934 + * @param int $quiz_count Total quiz count.
2935 + * @param int $assignment_count Total assignment count.
2936 + *
2937 + * @return string
2938 + */
2939 + private static function get_course_completion_requirement_message( $quiz_count, $assignment_count ) {
2940 + $quiz_label = _n( 'quiz', 'quizzes', $quiz_count, 'tutor' );
2941 + $assignment_label = _n( 'assignment', 'assignments', $assignment_count, 'tutor' );
426 2942
427 - do_action('tutor_course_complete_after', $course_id);
2943 + if ( $quiz_count && ! $assignment_count ) {
2944 + return sprintf(
2945 + /* translators: %1$s: item count; %2$s: item label */
2946 + __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2947 + $quiz_count,
2948 + $quiz_label
2949 + );
2950 + }
428 2951
429 - wp_redirect(get_the_permalink($course_id));
2952 + if ( ! $quiz_count && $assignment_count ) {
2953 + return sprintf(
2954 + /* translators: %1$s: item count; %2$s: item label */
2955 + __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2956 + $assignment_count,
2957 + $assignment_label
2958 + );
2959 + }
2960 +
2961 + return sprintf(
2962 + /* translators: %1$s: quiz count; %2$s: quiz label; %3$s: assignment count; %4$s: assignment label */
2963 + __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ),
2964 + $quiz_count,
2965 + $quiz_label,
2966 + $assignment_count,
2967 + $assignment_label
2968 + );
430 2969 }
431 2970
432 -
433 - public function tutor_load_instructors_modal(){
2971 + /**
2972 + * Check if all lessons and quizzes done before mark course complete.
2973 + *
2974 + * @since 1.5.8
2975 + *
2976 + * @param string $html HTML string.
2977 + *
2978 + * @return string
2979 + */
2980 + public function tutor_lms_hide_course_complete_btn( $html ) {
2981 + $_msg = self::get_course_completion_restrict_msg();
2982 +
2983 + if ( $_msg ) {
2984 + return '<div class="tutor-alert tutor-warning tutor-mt-28">
2985 + <div class="tutor-alert-text">
2986 + <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2987 + <span>' . $_msg . '</span>
2988 + </div>
2989 + </div>';
2990 + }
2991 +
2992 + return $html;
2993 + }
2994 +
2995 + /**
2996 + * Generate Gradebook
2997 + *
2998 + * @since 1.5.8
2999 + *
3000 + * @param string $html HTML string.
3001 + *
3002 + * @return string
3003 + */
3004 + public function get_generate_greadbook( $html ) {
3005 + if ( ! tutor_utils()->is_completed_course() ) {
3006 + return '';
3007 + }
3008 + return $html;
3009 + }
3010 +
3011 + /**
3012 + * Add social share content in header
3013 + *
3014 + * @since 1.6.3
3015 + *
3016 + * @return void
3017 + */
3018 + public function social_share_content() {
3019 + global $wp_query, $post;
3020 + if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
3021 + <!--Facebook-->
3022 + <meta property="og:type" content="website"/>
3023 + <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
3024 + <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
3025 + <!--Twitter-->
3026 + <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
3027 + <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
3028 + <!--Google+-->
3029 + <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
3030 + <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
3031 + <?php
3032 + }
3033 + }
3034 +
3035 + /**
3036 + * Delete associated enrollment
3037 + *
3038 + * @since 1.8.2
3039 + *
3040 + * @param integer $post_id post ID.
3041 + *
3042 + * @return void
3043 + */
3044 + public function delete_associated_enrollment( $post_id ) {
434 3045 global $wpdb;
435 3046
436 - $course_id = (int) sanitize_text_field($_POST['course_id']);
437 - $search_terms = sanitize_text_field(tutor_utils()->avalue_dot('search_terms', $_POST));
3047 + $enroll_id = $wpdb->get_var(
3048 + $wpdb->prepare(
3049 + "SELECT
3050 + post_id
3051 + FROM
3052 + {$wpdb->postmeta}
3053 + WHERE
3054 + meta_key=%s
3055 + AND meta_value = %d
3056 + ",
3057 + EnrollmentModel::ENROLLMENT_ORDER_ID_META,
3058 + $post_id
3059 + )
3060 + );
438 3061
439 - $saved_instructors = tutor_utils()->get_instructors_by_course($course_id);
3062 + if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
440 3063
441 - $instructors = array();
3064 + $course_id = get_post_field( 'post_parent', $enroll_id );
3065 + $user_id = get_post_field( 'post_author', $enroll_id );
442 3066
3067 + tutor_utils()->cancel_course_enrol( $course_id, $user_id );
3068 + }
3069 + }
443 3070
444 - $not_in_sql = apply_filters('tutor_instructor_query_when_exists', " AND ID <1 ");
3071 + /**
3072 + * Reset course progress.
3073 + *
3074 + * @since 1.5.8
3075 + *
3076 + * @return void
3077 + */
3078 + public function tutor_reset_course_progress() {
3079 + tutor_utils()->checking_nonce();
3080 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3081 + $context = Input::post( 'context', '' );
3082 + $course_reset_progress = tutor_utils()->get_option( 'course_reset_progress', false );
3083 + $course_retake_feature = tutor_utils()->get_option( 'course_retake_feature', false );
445 3084
446 - if ($saved_instructors){
447 - $saved_instructors_ids = wp_list_pluck($saved_instructors, 'ID');
448 - $instructor_not_in_ids = implode(',', $saved_instructors_ids);
449 - $not_in_sql .= "AND ID NOT IN($instructor_not_in_ids) ";
3085 + if ( ! $course_reset_progress && 'learning-area-sidebar' === $context ) {
3086 + $this->response_bad_request( __( 'You do not have permission to reset this course.', 'tutor' ) );
3087 + return;
450 3088 }
451 3089
452 - $search_sql = '';
453 - if ($search_terms){
454 - $search_sql = "AND user_login like '%{$search_terms}%' or user_nicename like '%{$search_terms}%' or display_name like '%{$search_terms}%' ";
3090 + if ( ! $course_retake_feature && ( 'course-landing' === $context || 'learning-area' === $context ) ) {
3091 + $this->response_bad_request( __( 'You do not have permission to retake this course.', 'tutor' ) );
3092 + return;
455 3093 }
456 3094
457 - $instructors = $wpdb->get_results("select ID, display_name from {$wpdb->users}
458 - INNER JOIN {$wpdb->usermeta} ON ID = user_id AND meta_key = '_tutor_instructor_status' AND meta_value = 'approved'
459 - WHERE ID > 0 {$not_in_sql} {$search_sql} limit 10 ");
3095 + if ( ! $course_id || ! is_numeric( $course_id ) || ! EnrollmentModel::is_enrolled( $course_id ) ) {
3096 + $this->response_bad_request( __( 'Invalid Course ID or Access Denied.', 'tutor' ) );
3097 + return;
3098 + }
460 3099
461 - $output = '';
462 - if (is_array($instructors) && count($instructors)){
463 - $instructor_output = '';
464 - foreach ($instructors as $instructor){
465 - $instructor_output .= "<p><label><input type='radio' name='tutor_instructor_ids[]' value='{$instructor->ID}' > {$instructor->display_name} </label></p>";
3100 + tutor_utils()->delete_course_progress( $course_id );
3101 + $this->json_response( '', array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
3102 + }
3103 +
3104 + /**
3105 + * Do enroll if guest attempt to enroll and course is free
3106 + *
3107 + * @since 1.9.8
3108 + *
3109 + * @param integer $course_id course ID.
3110 + * @param integer $user_id user ID.
3111 + *
3112 + * @return void
3113 + */
3114 + public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
3115 + $course_id = sanitize_text_field( $course_id );
3116 + $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
3117 +
3118 + if ( $course_id && $is_allowed ) {
3119 + $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
3120 + if ( ! $is_purchasable ) {
3121 + EnrollmentModel::do_enroll( $course_id, $order_id = 0, $user_id );
3122 + do_action( 'guest_attempt_after_enrollment', $course_id );
466 3123 }
3124 + }
3125 + }
467 3126
468 - $output .= apply_filters('tutor_course_instructors_html', $instructor_output, $instructors);
469 - $output .= '<p class="quiz-search-suggest-text">'.__('Search to get the specific instructors', 'tutor').'</p>';
3127 + /**
3128 + * Handle course enrollment
3129 + *
3130 + * @since 2.1.0
3131 + *
3132 + * @return void
3133 + */
3134 + public function course_enrollment() {
3135 + tutor_utils()->checking_nonce();
470 3136
471 - }else{
472 - $output .= __('No instructor available or you have already added maximum instructors', 'tutor');
3137 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3138 + $user_id = get_current_user_id();
3139 +
3140 + if ( ! $course_id || ! $user_id ) {
3141 + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
473 3142 }
474 3143
3144 + $password_protected = post_password_required( $course_id );
3145 + if ( $password_protected ) {
3146 + wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3147 + }
475 3148
476 - if ( ! defined('TUTOR_MT_VERSION')){
477 - $output .= '<p class="tutor-notice-warning" style="margin-top: 50px; font-size: 14px;">'. sprintf( __('To add unlimited multiple instructors in your course, get %sTutor LMS Pro%s', 'tutor'), '<a href="https://www.themeum.com/product/tutor-lms" target="_blank">', "</a>" ) .'</p>';
3149 + $course = get_post( $course_id );
3150 +
3151 + if ( CourseModel::STATUS_PRIVATE === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3152 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
478 3153 }
479 3154
480 - wp_send_json_success(array('output' => $output));
3155 + /**
3156 + * This check was added to address a security issue where users could
3157 + * enroll in a course via an AJAX call without purchasing it.
3158 + *
3159 + * To prevent this, we now verify whether the course is paid.
3160 + * Additionally, we check if the user is already enrolled, since
3161 + * Tutor's default behavior enrolls users automatically upon purchase.
3162 + *
3163 + * @since 3.9.4
3164 + */
3165 + if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3166 + $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id );
3167 +
3168 + if ( ! $is_enrolled ) {
3169 + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3170 + }
3171 + }
3172 +
3173 + $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id );
3174 + if ( $enroll ) {
3175 + wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3176 + } else {
3177 + wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3178 + }
481 3179 }
482 3180
483 - public function tutor_add_instructors_to_course(){
484 - $course_id = (int) sanitize_text_field($_POST['course_id']);
485 - $instructor_ids = tutor_utils()->avalue_dot('tutor_instructor_ids', $_POST);
3181 + /**
3182 + * After trash a course direct to the course list page
3183 + *
3184 + * @since 2.1.7
3185 + *
3186 + * @param integer $post_id int course id.
3187 + *
3188 + * @return void
3189 + */
3190 + public static function redirect_to_course_list_page( int $post_id ): void {
3191 + $post = get_post( $post_id );
3192 + if ( tutor()->course_post_type === $post->post_type ) {
3193 + $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
3194 + if ( ! $is_gutenberg_enabled ) {
3195 + wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
3196 + exit;
3197 + }
3198 + }
3199 + }
486 3200
487 - if (is_array($instructor_ids) && count($instructor_ids)){
488 - foreach ($instructor_ids as $instructor_id){
489 - add_user_meta($instructor_id, '_tutor_instructor_course_id', $course_id);
3201 + /**
3202 + * Create or update WooCommerce product
3203 + *
3204 + * If product id not set it will create new one.
3205 + *
3206 + * @since 2.2.0
3207 + *
3208 + * @param string $title product title.
3209 + * @param string $reg_price product price.
3210 + * @param string $sale_price product sale price.
3211 + * @param int $product_id product ID.
3212 + * @param string $status product status.
3213 + *
3214 + * @return integer Product id or return 0 if WC not exists
3215 + */
3216 + public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
3217 + if ( ! tutor_utils()->has_wc() ) {
3218 + return 0;
3219 + }
3220 +
3221 + $product_obj = new \WC_Product();
3222 + if ( $product_id ) {
3223 + $product_obj = wc_get_product( $product_id );
3224 + }
3225 +
3226 + $product_obj->set_name( $title );
3227 + $product_obj->set_status( $status );
3228 + $product_obj->set_price( $reg_price );
3229 + $product_obj->set_regular_price( $reg_price );
3230 +
3231 + if ( $sale_price > 0 ) {
3232 + $product_obj->set_sale_price( $sale_price );
3233 + } else {
3234 + $product_obj->set_sale_price( null );
3235 + }
3236 +
3237 + $product_obj->set_sold_individually( true );
3238 +
3239 + return $product_obj->save();
3240 + }
3241 +
3242 + /**
3243 + * Get course/bundle mini info
3244 + *
3245 + * @since 3.0.0
3246 + *
3247 + * @param object $post Course or bundle post.
3248 + *
3249 + * @return array
3250 + */
3251 + public static function get_mini_info( object $post ) {
3252 + $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3253 + $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3254 + $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3255 + $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
3256 +
3257 + $info = array(
3258 + 'id' => $post->ID,
3259 + 'title' => $post->post_title,
3260 + 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3261 + 'is_purchasable' => $is_purchasable,
3262 + 'regular_price' => $regular_price,
3263 + 'sale_price' => $sale_price,
3264 + );
3265 +
3266 + $card_data = apply_filters( 'tutor_course_mini_info', $info, $post );
3267 +
3268 + return $card_data;
3269 + }
3270 +
3271 + /**
3272 + * Get course/bundle card data
3273 + *
3274 + * This method will return all data that contain in
3275 + * course card
3276 + *
3277 + * @since 3.0.0
3278 + *
3279 + * @param object $post Course or bundle post.
3280 + *
3281 + * @return array
3282 + */
3283 + public static function get_card_data( object $post ) {
3284 + $info = self::get_mini_info( $post );
3285 +
3286 + $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3287 + $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3288 + $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3289 +
3290 + $card_data = apply_filters( 'tutor_course_card_data', $info, $post );
3291 +
3292 + return $card_data;
3293 + }
3294 +
3295 + /**
3296 + * Filter user list access for instructor
3297 + *
3298 + * @since 3.0.0
3299 + *
3300 + * @param bool $access access.
3301 + *
3302 + * @return bool
3303 + */
3304 + public function user_list_access_for_instructor( $access ) {
3305 + $is_instructor = User::is_instructor();
3306 + return $access || $is_instructor;
3307 + }
3308 +
3309 + /**
3310 + * Filter user list args for instructor
3311 + *
3312 + * @since 3.0.0
3313 + *
3314 + * @param array $args args.
3315 + *
3316 + * @return array
3317 + */
3318 + public function user_list_args_for_instructor( $args ) {
3319 + if ( User::is_instructor() ) {
3320 + if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3321 + unset( $args['fields']['user_email'] );
490 3322 }
491 3323 }
492 3324
493 - $saved_instructors = tutor_utils()->get_instructors_by_course($course_id);
494 - $output = '';
3325 + $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3326 + if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3327 + $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3328 + }
495 3329
496 - if ($saved_instructors){
497 - foreach ($saved_instructors as $t){
3330 + return $args;
3331 + }
498 3332
499 - $output .= '<div id="added-instructor-id-'.$t->ID.'" class="added-instructor-item added-instructor-item-'.$t->ID.'" data-instructor-id="'.$t->ID.'">
500 - <span class="instructor-icon">'.get_avatar($t->ID, 30).'</span>
501 - <span class="instructor-name"> '.$t->display_name.' </span>
502 - <span class="instructor-control">
503 - <a href="javascript:;" class="tutor-instructor-delete-btn"><i class="tutor-icon-garbage"></i></a>
504 - </span>
505 - </div>';
3333 + /**
3334 + * Get a list of possible course status.
3335 + *
3336 + * @since 3.6.2
3337 + *
3338 + * @return array
3339 + */
3340 + public static function course_status_list() {
3341 + return array(
3342 + 'publish',
3343 + 'private',
3344 + 'draft',
3345 + 'trash',
3346 + 'pending',
3347 + 'future',
3348 + );
3349 + }
3350 +
3351 + /**
3352 + * Link a course/bundle post to a WooCommerce product.
3353 + *
3354 + * @since 3.8.2
3355 + *
3356 + * @param int $post_ID The WordPress post ID of the course.
3357 + * @param int $product_id The WooCommerce product ID to associate with the course.
3358 + *
3359 + * @return void
3360 + */
3361 + public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3362 +
3363 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
3364 +
3365 + // Mark product for woocommerce.
3366 + update_post_meta( $product_id, '_virtual', 'yes' );
3367 + update_post_meta( $product_id, '_tutor_product', 'yes' );
3368 + }
3369 +
3370 + /**
3371 + * Map Tutor's course prices to WooCommerce.
3372 + *
3373 + * @since 3.8.2
3374 + *
3375 + * @param int $post_ID The WordPress post ID of the course.
3376 + * @param string|int|float $regular_price The regular price.
3377 + * @param string|int|float $sale_price The sale price.
3378 + * @return void
3379 + */
3380 + private static function set_course_regular_and_sale_price( $post_ID, $regular_price, $sale_price ) {
3381 +
3382 + // Set course regular & sale price.
3383 + update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3384 + update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3385 + }
3386 +
3387 + /**
3388 + * Render start/resume button in enrolled course action btn.
3389 + *
3390 + * @since 4.0.0
3391 + *
3392 + * @param int $course_id course id.
3393 + *
3394 + * @return void
3395 + */
3396 + public function render_course_action_btn( int $course_id ) {
3397 + $is_completed = tutor_utils()->is_completed_course( $course_id );
3398 + if ( $is_completed ) {
3399 + $certificate_addon_active = tutor_utils()->is_addon_enabled( 'tutor-certificate' );
3400 + if ( ! $certificate_addon_active ) {
3401 + Button::make()
3402 + ->tag( 'a' )
3403 + ->label( __( 'Completed', 'tutor' ) )
3404 + ->icon( Icon::COMPLETED_CIRCLE )
3405 + ->variant( Variant::PRIMARY )
3406 + ->size( Size::X_SMALL )
3407 + ->attr( 'href', esc_url( get_the_permalink( $course_id ) ) )
3408 + ->render();
506 3409 }
3410 + return;
507 3411 }
508 3412
509 - wp_send_json_success(array('output' => $output));
3413 + $course_progress = tutor_utils()->get_course_completed_percent( $course_id, 0, true );
3414 + $button_text = $course_progress['completed_percent'] > 0 ? __( 'Resume', 'tutor' ) : __( 'Start', 'tutor' );
3415 + $button_url = tutor_utils()->get_course_first_lesson( $course_id );
3416 +
3417 + if ( ! $button_url ) {
3418 + $button_url = get_the_permalink( $course_id );
3419 + }
3420 +
3421 + Button::make()
3422 + ->tag( 'a' )
3423 + ->label( $button_text )
3424 + ->icon( Icon::PLAY_2 )
3425 + ->variant( Variant::PRIMARY )
3426 + ->size( Size::X_SMALL )
3427 + ->attr( 'href', esc_url( $button_url ) )
3428 + ->render();
510 3429 }
511 3430
512 - public function detach_instructor_from_course(){
513 - global $wpdb;
3431 + /**
3432 + * Get the content for the course completion modal.
3433 + *
3434 + * This method returns HTML for displaying a modal that informs the user
3435 + * that they have not completed all required lessons and assessments. It includes
3436 + * the user's current progress shown as a percentage and a progress bar.
3437 + *
3438 + * @since 4.0.0
3439 + *
3440 + * @param float $course_progress The completion percentage of the course.
3441 + *
3442 + * @return string The rendered HTML content for the modal.
3443 + */
3444 + public static function get_complete_modal_content( float $course_progress = 0 ): string {
3445 + ob_start();
3446 + ?>
3447 + <div>
3448 + <p class="tutor-p3 tutor-text-secondary tutor-text-center tutor-mb-7 tutor-px-11">
3449 + <?php esc_html_e( 'You have not completed all required lessons and assessments. ', 'tutor' ); ?>
3450 + </p>
3451 + <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">
3452 + <div class="tutor-flex tutor-items-center tutor-justify-between">
3453 + <div class="tutor-p3 tutor-text-secondary">
3454 + <?php esc_html_e( 'Your Progress', 'tutor' ); ?>
3455 + </div>
3456 + <div class="tutor-p1 tutor-font-bold">
3457 + <?php echo esc_html( number_format_i18n( $course_progress ) . '%' ); ?>
3458 + </div>
3459 + </div>
3460 + <?php
3461 + Progress::make()->type( 'bar' )->value( $course_progress )->render();
3462 + ?>
3463 + </div>
3464 + </div>
3465 + <?php
3466 + return ob_get_clean();
3467 + }
514 3468
515 - $instructor_id = (int) sanitize_text_field($_POST['instructor_id']);
516 - $course_id = (int) sanitize_text_field($_POST['course_id']);
3469 + /**
3470 + * Render the course complete button.
3471 + *
3472 + * Displays a button that allows the user to complete the course. If the course
3473 + * progress is less than 100%, clicking the button shows a modal informing the user
3474 + * that not all requirements are fulfilled. If the course progress is 100%,
3475 + * clicking the button attempts to complete the course.
3476 + *
3477 + * @since 4.0.0
3478 + *
3479 + * @param string $modal_id The HTML ID of the modal to display if not complete.
3480 + * @param int $course_id The ID of the course.
3481 + * @param float $course_progress The current completion percentage of the course.
3482 + * @param string $size The button size.
3483 + * @param string $tooltip Optional. Tooltip message.
3484 + * @param bool $block Optional. Whether the button is full-width.
3485 + *
3486 + * @return void
3487 + */
3488 + 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 {
3489 + $button = Button::make()
3490 + ->variant( Variant::SECONDARY )
3491 + ->label( __( 'Complete the Course', 'tutor' ) )
3492 + ->icon( Icon::TICK_MARK )
3493 + ->size( $size )
3494 + ->block( $block )
3495 + ->attr( 'type', 'button' );
517 3496
518 - $wpdb->delete($wpdb->usermeta, array('user_id' => $instructor_id, 'meta_key' => '_tutor_instructor_course_id', 'meta_value' => $course_id) );
519 - wp_send_json_success();
3497 + if ( ! empty( $tooltip ) ) {
3498 + $button->disabled();
3499 + }
3500 +
3501 + if ( $course_progress < 100 ) {
3502 + $button->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" );
3503 + } else {
3504 + $button->attr( '@click', "handleCourseComplete({$course_id})" );
3505 + $button->attr( ':class', "courseCompleteMutation?.isPending ? 'tutor-btn-loading' : ''" );
3506 + $button->attr( ':disabled', 'courseCompleteMutation?.isPending' );
3507 + }
3508 +
3509 + if ( ! empty( $tooltip ) ) {
3510 + Tooltip::make()
3511 + ->content( $tooltip )
3512 + ->placement( Tooltip::PLACEMENT_BOTTOM )
3513 + ->arrow( Tooltip::ARROW_CENTER )
3514 + ->trigger_element( $button->get() )
3515 + ->render();
3516 + } else {
3517 + $button->render();
3518 + }
520 3519 }
521 3520
522 -
523 -}
3521 + /**
3522 + * Render course retake button
3523 + *
3524 + * @since 4.0.0
3525 + *
3526 + * @param string $modal_id Modal id.
3527 + * @param string $size The button size.
3528 + * @param bool $block Optional. Whether the button is full-width.
3529 + *
3530 + * @return void
3531 + */
3532 + public static function render_course_retake_btn( string $modal_id, string $size = Size::MEDIUM, bool $block = false ): void {
3533 + Button::make()
3534 + ->variant( Variant::SECONDARY )
3535 + ->label( __( 'Retake this Course', 'tutor' ) )
3536 + ->icon( Icon::RELOAD_4 )
3537 + ->size( $size )
3538 + ->block( $block )
3539 + ->attr( 'type', 'button' )
3540 + ->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" )
3541 + ->render();
3542 + }
3543 +}