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