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