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