PluginProbe
Tutor LMS – eLearning and online course solution / 3.9.2
Tutor LMS – eLearning and online course solution v3.9.2
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
tutor / classes / Course.php

Course.php in Tutor LMS – eLearning and online course solution 3.9.2, at classes/Course.php

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