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