PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.0.0
Tutor LMS – eLearning and online course solution v3.0.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 3 years ago WooCommerce.php 1 year ago
Course.php
3038 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 $quiz_option = get_post_meta( $post->ID, Quiz::META_QUIZ_OPTION, true );
1081 if ( isset( $quiz_option['quiz_type'] ) && 'tutor_h5p_quiz' === $quiz_option['quiz_type'] ) {
1082 $post->quiz_type = 'tutor_h5p_quiz';
1083 // remove h5p quiz if tutor h5p addon is disabled.
1084 $is_h5p_enabled = tutor()->has_pro && \TutorPro\H5P\H5P::is_enabled();
1085 if ( ! $is_h5p_enabled ) {
1086 continue;
1087 }
1088 }
1089 $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1090 $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
1091 }
1092
1093 array_push( $current_topic['contents'], $post );
1094 }
1095 }
1096
1097 array_push( $data, $current_topic );
1098 }
1099 }
1100
1101 return $data;
1102 }
1103
1104 /**
1105 * Get course contents
1106 *
1107 * @since 3.0.0
1108 */
1109 public function ajax_course_contents() {
1110 tutor_utils()->check_nonce();
1111
1112 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1113
1114 $this->check_access( $course_id );
1115
1116 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1117 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1118 }
1119
1120 if ( ! empty( $errors ) ) {
1121 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1122 }
1123
1124 $contents = $this->get_course_contents( $course_id );
1125
1126 $this->json_response(
1127 __( 'Course contents fetched successfully', 'tutor' ),
1128 $contents
1129 );
1130 }
1131
1132 /**
1133 * Get course details by ID
1134 *
1135 * @since 3.0.0
1136 *
1137 * @return void
1138 */
1139 public function ajax_course_details() {
1140 tutor_utils()->check_nonce();
1141
1142 $errors = array();
1143 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1144
1145 $this->check_access( $course_id );
1146
1147 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1148 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1149 }
1150
1151 if ( ! empty( $errors ) ) {
1152 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1153 }
1154
1155 $price_type = tutor_utils()->price_type( $course_id );
1156 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1157
1158 $product_name = '';
1159 $price = 0;
1160 $sale_price = 0;
1161 $product_id = tutor_utils()->get_course_product_id( $course_id );
1162
1163 if ( 'wc' === $monetize_by ) {
1164 $product = wc_get_product( $product_id );
1165 if ( $product ) {
1166 $product_name = $product->get_name();
1167 $price = $product->get_regular_price();
1168 $sale_price = $product->get_sale_price();
1169 }
1170 }
1171
1172 if ( 'tutor' === $monetize_by ) {
1173 $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1174 $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1175 }
1176
1177 $course_pricing = array(
1178 'type' => $price_type,
1179 'product_id' => $product_id,
1180 'product_name' => $product_name,
1181 'price' => $price,
1182 'sale_price' => $sale_price,
1183 );
1184
1185 $video_intro = get_post_meta( $course_id, '_video', true );
1186 if ( $video_intro ) {
1187 $source = $video_intro['source'] ?? '';
1188 if ( 'html5' === $source ) {
1189 $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1190 $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1191 $video['poster_url'] = $poster_url;
1192 $video['source_html5'] = $source_html5;
1193 }
1194 }
1195
1196 $course = get_post( $course_id, ARRAY_A );
1197 $editors = tutor_utils()->get_editor_list( $course_id );
1198
1199 $data = array(
1200 'editors' => array_values( $editors ),
1201 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1202 'preview_link' => get_preview_post_link( $course_id ),
1203 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1204 'course_categories' => wp_get_post_terms( $course_id, 'course-category' ),
1205 'course_tags' => wp_get_post_terms( $course_id, 'course-tag' ),
1206 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1207 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
1208
1209 'enable_qna' => get_post_meta( $course_id, '_tutor_enable_qa', true ),
1210 'is_public_course' => get_post_meta( $course_id, '_tutor_is_public_course', true ),
1211 'course_level' => get_post_meta( $course_id, '_tutor_course_level', true ),
1212 'video' => $video_intro,
1213 'course_duration' => get_post_meta( $course_id, '_course_duration', true ),
1214 'course_benefits' => get_post_meta( $course_id, '_tutor_course_benefits', true ),
1215 'course_requirements' => get_post_meta( $course_id, '_tutor_course_requirements', true ),
1216 'course_target_audience' => get_post_meta( $course_id, '_tutor_course_target_audience', true ),
1217 'course_material_includes' => get_post_meta( $course_id, '_tutor_course_material_includes', true ),
1218 'monetize_by' => $monetize_by,
1219 'course_pricing' => $course_pricing,
1220 'course_settings' => get_post_meta( $course_id, '_tutor_course_settings', true ),
1221 'step_completion_status' => array(
1222 'basic' => true,
1223 'curriculum' => false,
1224 'additional' => false,
1225 'certificate' => false,
1226 ),
1227 );
1228
1229 $data = apply_filters( 'tutor_course_details_response', array_merge( $course, $data ) );
1230
1231 $this->json_response( __( 'Data retrieved successfully!' ), $data );
1232 }
1233
1234 /**
1235 * Load course builder.
1236 *
1237 * @since 3.0.0
1238 *
1239 * @return void
1240 */
1241 public function load_course_builder() {
1242 global $pagenow;
1243
1244 $has_pro = tutor()->has_pro;
1245 $has_access_role = User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) );
1246
1247 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1248 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1249 $backend_edit = $backend_builder && $course_id;
1250
1251 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1252 $frontend_edit = $is_frontend_builder && $course_id;
1253
1254 if ( $has_access_role && ( $backend_edit || ( $has_pro && $frontend_edit ) ) ) {
1255 $post_type = get_post_type( $course_id );
1256 $can_edit_course = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
1257
1258 if ( tutor()->course_post_type === $post_type && ( User::is_admin() || $can_edit_course ) ) {
1259 /**
1260 * Edit trash course behavior
1261 *
1262 * @since 3.0.0
1263 */
1264 if ( CourseModel::STATUS_TRASH === get_post_status( $course_id ) ) {
1265 $message = User::is_admin()
1266 ? __( 'You cannot edit this course because it is in the Trash. Please restore it and try again', 'tutor' )
1267 : tutor_utils()->error_message();
1268 wp_die( esc_html( $message ) );
1269 }
1270
1271 $this->load_course_builder_view();
1272 }
1273 }
1274 }
1275
1276 /**
1277 * Enqueue course builder assets like CSS, JS
1278 *
1279 * @since 3.0.0
1280 *
1281 * @return void
1282 */
1283 public function enqueue_course_builder_assets() {
1284 // Fix: function print_emoji_styles is deprecated since version 6.4.0!
1285 remove_action( 'wp_print_styles', 'print_emoji_styles' );
1286
1287 do_action( 'tutor_course_builder_before_wp_editor_load' );
1288 wp_enqueue_script( 'wp-tinymce' );
1289 wp_enqueue_script( 'mce-view' );
1290 wp_enqueue_editor();
1291
1292 wp_enqueue_media();
1293 wp_enqueue_script( 'tutor-shared', tutor()->url . 'assets/js/tutor-shared.min.js', array( 'wp-i18n', 'wp-element' ), TUTOR_VERSION, true );
1294 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 );
1295
1296 $default_data = ( new Assets( false ) )->get_default_localized_data();
1297
1298 if ( isset( $default_data['current_user']->data ) ) {
1299 $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']->data->ID );
1300 $default_data['current_user']->data->tutor_profile_photo_url = $tutor_user->tutor_profile_photo_url;
1301 }
1302
1303 /**
1304 * Localized only options to protect sensitive info like API keys.
1305 */
1306 $required_options = array(
1307 'monetize_by',
1308 'enable_course_marketplace',
1309 'course_permalink_base',
1310 'supported_video_sources',
1311 'enrollment_expiry_enabled',
1312 'enable_q_and_a_on_course',
1313 'instructor_can_delete_course',
1314 'chatgpt_enable',
1315 'hide_admin_bar_for_users',
1316 'enable_redirect_on_course_publish_from_frontend',
1317 'instructor_can_publish_course',
1318 );
1319
1320 $full_settings = get_option( 'tutor_option', array() );
1321 $settings = Options_V2::get_only( $required_options );
1322 $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1323 $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1324 $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1325
1326 $new_data = array( 'settings' => $settings );
1327
1328 $data = array_merge( $default_data, $new_data );
1329
1330 /**
1331 * Course builder dashboard URL based on role and settings.
1332 */
1333 $dashboard_url = tutor_utils()->tutor_dashboard_url();
1334 if ( User::is_admin() ) {
1335 $dashboard_url = get_admin_url();
1336 }
1337
1338 /**
1339 * EDD product list
1340 */
1341 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1342 if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1343 $data['edd_products'] = tutor_utils()->get_edd_products();
1344 }
1345
1346 $difficulty_levels = array();
1347 foreach ( tutor_utils()->course_levels() as $value => $label ) {
1348 $difficulty_levels[] = array(
1349 'label' => $label,
1350 'value' => $value,
1351 );
1352 }
1353
1354 $supported_video_sources = array();
1355 $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1356
1357 foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1358 if ( in_array( $value, $saved_video_source_list, true ) ) {
1359 $supported_video_sources[] = array(
1360 'label' => $label,
1361 'value' => $value,
1362 );
1363 }
1364 }
1365
1366 $data['dashboard_url'] = $dashboard_url;
1367 $data['backend_course_list_url'] = get_admin_url( null, '?page=tutor' );
1368 $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1369 $data['timezones'] = tutor_global_timezone_lists();
1370 $data['difficulty_levels'] = $difficulty_levels;
1371 $data['supported_video_sources'] = $supported_video_sources;
1372 $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1373 $data['max_upload_size'] = size_format( wp_max_upload_size() );
1374
1375 $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1376
1377 wp_localize_script(
1378 'mce-view',
1379 'mceViewL10n',
1380 array(
1381 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1382 )
1383 );
1384
1385 wp_localize_script( 'tutor-course-builder', '_tutorobject', $data );
1386 wp_set_script_translations( 'tutor-course-builder', 'tutor', tutor()->path . 'languages/' );
1387 }
1388
1389 /**
1390 * Load wp editor modal
1391 *
1392 * @since 3.0.0
1393 *
1394 * @return void
1395 */
1396 public function load_wp_link_modal() {
1397 if ( is_admin() ) {
1398 include_once tutor()->path . 'views/modal/wp-editor-link.php';
1399 }
1400 }
1401
1402 /**
1403 * Load view for course builder.
1404 *
1405 * @since 3.0.0
1406 *
1407 * @return void
1408 */
1409 public function load_course_builder_view() {
1410 do_action( 'tutor_before_course_builder_load' );
1411 include_once tutor()->path . 'views/pages/course-builder.php';
1412 do_action( 'tutor_after_course_builder_load' );
1413 exit( 0 );
1414 }
1415
1416 /**
1417 * Add enroll require login class
1418 *
1419 * @since 2.6.0
1420 *
1421 * @param string $class_name css class name.
1422 *
1423 * @return string
1424 */
1425 public function add_enroll_required_login_class( $class_name ) {
1426 $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1427 if ( ! $enabled_tutor_login ) {
1428 return '';
1429 }
1430
1431 return $class_name;
1432 }
1433
1434 /**
1435 * Get list of WC products.
1436 *
1437 * @since 2.5.0
1438 * @since 3.0.0 exclude_linked_products, course_id are added.
1439 *
1440 * @return void
1441 */
1442 public function get_wc_products() {
1443 $exclude = array();
1444 $exclude_linked_products = Input::has( 'exclude_linked_products' );
1445 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1446
1447 if ( $exclude_linked_products ) {
1448 $exclude = tutor_utils()->get_linked_product_ids();
1449 }
1450
1451 if ( $course_id ) {
1452 $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1453 if ( $linked_product_id ) {
1454 $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1455 }
1456 }
1457
1458 $exclude = array_unique( $exclude );
1459
1460 $this->json_response(
1461 __( 'Products retrieved successfully!', 'tutor' ),
1462 tutor_utils()->get_wc_products_db( $exclude ),
1463 HttpHelper::STATUS_OK
1464 );
1465 }
1466
1467 /**
1468 * Get course associate WC product info by Ajax request
1469 *
1470 * @since 2.0.7
1471 *
1472 * @return void
1473 */
1474 public function get_wc_product() {
1475 tutor_utils()->checking_nonce();
1476 $product_id = Input::post( 'product_id' );
1477 $product = wc_get_product( $product_id );
1478 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1479
1480 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1481
1482 /**
1483 * If selected product is already linked with
1484 * a course & it is not the current course the
1485 * return error
1486 *
1487 * @since 2.1.0
1488 */
1489 if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1490 wp_send_json_error(
1491 __( 'One product can not be added to multiple course!', 'tutor' )
1492 );
1493 }
1494
1495 if ( $product ) {
1496 $data = array(
1497 'name' => $product->get_name(),
1498 'regular_price' => $product->get_regular_price(),
1499 'sale_price' => $product->get_sale_price(),
1500 );
1501 wp_send_json_success( $data );
1502 } else {
1503 wp_send_json_error( __( 'Product not found', 'tutor' ) );
1504 }
1505 }
1506
1507 /**
1508 * Update course content order
1509 *
1510 * @since 1.0.0
1511 * @return void
1512 */
1513 public function tutor_update_course_content_order() {
1514 tutor_utils()->checking_nonce();
1515
1516 if ( Input::has( 'content_parent' ) ) {
1517 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1518 $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1519 $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1520
1521 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1522 wp_send_json_success( array( 'message' => __( 'Access Denied!', 'tutor' ) ) );
1523 exit;
1524 }
1525
1526 // Update the parent topic id of the content.
1527 global $wpdb;
1528 $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1529 }
1530
1531 // Save course content order.
1532 $this->save_course_content_order();
1533
1534 wp_send_json_success();
1535 }
1536
1537 /**
1538 * Restrict new student entry
1539 *
1540 * @since 1.0.0
1541 * @param mixed $content content.
1542 * @return mixed
1543 */
1544 public function restrict_new_student_entry( $content ) {
1545
1546 if ( ! tutor_utils()->is_course_fully_booked() ) {
1547 // No restriction if not fully booked.
1548 return $content;
1549 }
1550
1551 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">' .
1552 __( 'Fully Booked', 'tutor' )
1553 . '</div></div>';
1554 }
1555
1556 /**
1557 * Restrict media
1558 *
1559 * @since 1.0.0
1560 * @param string $where where clause.
1561 * @return string
1562 */
1563 public function restrict_media( $where ) {
1564 $action = Input::post( 'action' );
1565 if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1566 if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1567 $where .= ' AND post_author=' . get_current_user_id();
1568 }
1569 }
1570
1571 return $where;
1572 }
1573
1574 /**
1575 * Course meta box (Topics)
1576 *
1577 * @since 1.0.0
1578 * @param boolean $echo display or not.
1579 * @return string
1580 */
1581 public function course_meta_box( $echo = true ) {
1582 $file_path = tutor()->path . 'views/metabox/course-topics.php';
1583
1584 if ( $echo ) {
1585 /**
1586 * Use echo raise WPCS security issue
1587 * Helper wp_kses_post break content.
1588 */
1589 include $file_path;
1590 } else {
1591 ob_start();
1592 include $file_path;
1593 return ob_get_clean();
1594 }
1595 }
1596
1597 /**
1598 * Save course content order
1599 *
1600 * @since 1.0.0
1601 * @return void
1602 */
1603 private function save_course_content_order() {
1604 global $wpdb;
1605
1606 $new_order = Input::post( 'tutor_topics_lessons_sorting' );
1607 if ( ! empty( $new_order ) ) {
1608 $order = json_decode( $new_order, true );
1609
1610 if ( is_array( $order ) && count( $order ) ) {
1611 $i = 0;
1612 foreach ( $order as $topic ) {
1613 $i++;
1614 $wpdb->update(
1615 $wpdb->posts,
1616 array( 'menu_order' => $i ),
1617 array( 'ID' => $topic['topic_id'] )
1618 );
1619
1620 /**
1621 * Removing All lesson with topic
1622 */
1623
1624 $wpdb->update(
1625 $wpdb->posts,
1626 array( 'post_parent' => 0 ),
1627 array( 'post_parent' => $topic['topic_id'] )
1628 );
1629
1630 /**
1631 * Lesson Attaching with topic ID
1632 * Sorting lesson
1633 */
1634 if ( isset( $topic['lesson_ids'] ) ) {
1635 $lesson_ids = $topic['lesson_ids'];
1636 } else {
1637 $lesson_ids = array();
1638 }
1639 if ( count( $lesson_ids ) ) {
1640 foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1641 $wpdb->update(
1642 $wpdb->posts,
1643 array(
1644 'post_parent' => $topic['topic_id'],
1645 'menu_order' => $lesson_key,
1646 ),
1647 array( 'ID' => $lesson_id )
1648 );
1649 }
1650 }
1651 }
1652 }
1653 }
1654 }
1655
1656 /**
1657 * Insert Topic and attached it with Course
1658 *
1659 * @since 1.0.0
1660 *
1661 * @param integer $post_ID post ID.
1662 * @param object $post post object.
1663 *
1664 * @return void
1665 */
1666 public function save_course_meta( $post_ID, $post ) {
1667 global $wpdb;
1668
1669 do_action( 'tutor_save_course', $post_ID, $post );
1670
1671 /**
1672 * Save course price type
1673 */
1674 $price_type = Input::post( 'tutor_course_price_type' );
1675 if ( $price_type ) {
1676 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
1677 }
1678
1679 //phpcs:disable WordPress.Security.NonceVerification.Missing
1680 // Course Duration.
1681 if ( ! empty( $_POST['course_duration'] ) ) {
1682 $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1683 update_post_meta( $post_ID, '_course_duration', $video );
1684 }
1685
1686 if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1687 $course_level = Input::post( '_tutor_course_level' );
1688 update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1689 }
1690
1691 $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1692 if ( $additional_data_edit ) {
1693 if ( ! empty( $_POST['course_benefits'] ) ) {
1694 $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1695 update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1696 } else {
1697 if ( ! tutor_is_rest() ) {
1698 delete_post_meta( $post_ID, '_tutor_course_benefits' );
1699 }
1700 }
1701
1702 if ( ! empty( $_POST['course_requirements'] ) ) {
1703 $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1704 update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1705 } else {
1706 if ( ! tutor_is_rest() ) {
1707 delete_post_meta( $post_ID, '_tutor_course_requirements' );
1708 }
1709 }
1710
1711 if ( ! empty( $_POST['course_target_audience'] ) ) {
1712 $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1713 update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1714 } else {
1715 if ( ! tutor_is_rest() ) {
1716 delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1717 }
1718 }
1719
1720 if ( ! empty( $_POST['course_material_includes'] ) ) {
1721 $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1722 update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1723 } else {
1724 if ( ! tutor_is_rest() ) {
1725 delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1726 }
1727 }
1728 //phpcs:enable WordPress.Security.NonceVerification.Missing
1729 }
1730
1731 /**
1732 * Sorting Topics and lesson
1733 */
1734 $this->save_course_content_order();
1735
1736 // Additional data like course intro video.
1737 if ( $additional_data_edit ) {
1738 // Sanitize data through helper method.
1739 $video = Input::sanitize_array(
1740 $_POST['video'] ?? array(), //phpcs:ignore
1741 array(
1742 'source_external_url' => 'esc_url',
1743 'source_embedded' => 'wp_kses_post',
1744 ),
1745 true
1746 );
1747 $video_source = tutor_utils()->array_get( 'source', $video );
1748 if ( -1 !== $video_source ) {
1749 update_post_meta( $post_ID, '_video', $video );
1750 } else {
1751 if ( ! tutor_is_rest() ) {
1752 delete_post_meta( $post_ID, '_video' );
1753 }
1754 }
1755 }
1756
1757 /**
1758 * Adding author to instructor automatically
1759 */
1760
1761 // Override post author id.
1762 $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1763 $attached = (int) $wpdb->get_var(
1764 $wpdb->prepare(
1765 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1766 WHERE user_id = %d
1767 AND meta_key = '_tutor_instructor_course_id'
1768 AND meta_value = %d ",
1769 $author_id,
1770 $post_ID
1771 )
1772 );
1773
1774 if ( ! $attached ) {
1775 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1776 }
1777
1778 /**
1779 * Disable question and answer for this course
1780 *
1781 * @since 1.7.0
1782 */
1783 if ( $additional_data_edit ) {
1784 foreach ( $this->additional_meta as $key ) {
1785 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1786 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1787 }
1788 }
1789
1790 do_action( 'tutor_save_course_after', $post_ID, $post );
1791 }
1792
1793 /**
1794 * Save course topic
1795 *
1796 * @since 1.0.0
1797 * @since 3.0.0 response and input name updated.
1798 *
1799 * @return void
1800 */
1801 public function tutor_save_topic() {
1802 tutor_utils()->check_nonce();
1803
1804 $is_update = false;
1805 $errors = array();
1806 $topic_title = Input::post( 'title' );
1807
1808 if ( empty( $topic_title ) ) {
1809 $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
1810 $this->json_response(
1811 __( 'Invalid inputs' ),
1812 $errors,
1813 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
1814 );
1815 }
1816
1817 // Gather parameters.
1818 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1819 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1820 $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
1821
1822 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
1823
1824 // Validate if user can manage the topic.
1825 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
1826 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1827 }
1828
1829 // Create payload to create/update the topic.
1830 $post_arr = array(
1831 'post_type' => 'topics',
1832 'post_title' => $topic_title,
1833 'post_content' => $topic_summary,
1834 'post_status' => 'publish',
1835 'post_author' => get_current_user_id(),
1836 'post_parent' => $course_id,
1837 'menu_order' => $next_topic_order_id,
1838 );
1839
1840 if ( $topic_id ) {
1841 $is_update = true;
1842 $post_arr['ID'] = $topic_id;
1843 }
1844
1845 $current_topic_id = wp_insert_post( $post_arr );
1846
1847 if ( $is_update ) {
1848 $this->json_response(
1849 __( 'Topic updated successfully!', 'tutor' ),
1850 $current_topic_id
1851 );
1852 } else {
1853 $this->json_response(
1854 __( 'Topic created successfully!', 'tutor' ),
1855 $current_topic_id,
1856 HttpHelper::STATUS_CREATED
1857 );
1858 }
1859 }
1860
1861 /**
1862 * Delete a course topic
1863 *
1864 * @since 1.0.0
1865 * @since 3.0.0 code refactor and response updated.
1866 *
1867 * @return void
1868 */
1869 public function tutor_delete_topic() {
1870 tutor_utils()->check_nonce();
1871
1872 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1873 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1874 $this->json_response(
1875 tutor_utils()->error_message(),
1876 null,
1877 HttpHelper::STATUS_FORBIDDEN
1878 );
1879 }
1880
1881 global $wpdb;
1882
1883 // Assign course ID to orphan content IDs since the topic will be deleted.
1884 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
1885 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
1886 foreach ( $content_ids as $content_id ) {
1887 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
1888 // Actually all kind of contents.
1889 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
1890 }
1891
1892 // Set contents under the topic orphan.
1893 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
1894
1895 // Then delete the topic from database.
1896 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
1897 wp_delete_post( $topic_id );
1898
1899 $this->json_response(
1900 __( 'Topic deleted successfully!', 'tutor' )
1901 );
1902 }
1903
1904 /**
1905 * Handle enroll now action
1906 *
1907 * @since 1.0.0
1908 *
1909 * @return void
1910 */
1911 public function enroll_now() {
1912
1913 if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
1914 return;
1915 }
1916
1917 // Checking Nonce.
1918 tutor_utils()->checking_nonce();
1919
1920 $user_id = get_current_user_id();
1921 if ( ! $user_id ) {
1922 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
1923 }
1924
1925 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
1926
1927 /**
1928 * TODO: need to check purchase information
1929 */
1930
1931 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
1932
1933 /**
1934 * If is is not purchasable, it's free, and enroll right now
1935 * If purchasable, then process purchase.
1936 *
1937 * @since: v.1.0.0
1938 */
1939 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
1940 // Process purchase.
1941
1942 } else {
1943 // Free enroll.
1944 tutor_utils()->do_enroll( $course_id );
1945 }
1946
1947 $referer_url = wp_get_referer();
1948 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
1949 exit;
1950 }
1951
1952 /**
1953 * Mark complete completed
1954 *
1955 * @since 1.0.0
1956 * @return void
1957 */
1958 public function mark_course_complete() {
1959 $tutor_action = Input::post( 'tutor_action' );
1960 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1961 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
1962 return;
1963 }
1964
1965 // Checking nonce.
1966 tutor_utils()->checking_nonce();
1967
1968 $user_id = get_current_user_id();
1969
1970 // TODO: need to show view if not signed_in.
1971 if ( ! $user_id ) {
1972 die( esc_html__( 'Please Sign-In', 'tutor' ) );
1973 }
1974
1975 CourseModel::mark_course_as_completed( $course_id, $user_id );
1976
1977 $permalink = get_the_permalink( $course_id );
1978
1979 // Set temporary identifier to show review pop up.
1980 self::set_review_popup_data( $user_id, $course_id, $permalink );
1981
1982 wp_safe_redirect( $permalink );
1983 exit;
1984 }
1985
1986 /**
1987 * Set data for review popup.
1988 *
1989 * @since 2.2.5
1990 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
1991 *
1992 * @param int $user_id user id.
1993 * @param int $course_id course id.
1994 *
1995 * @return void
1996 */
1997 public static function set_review_popup_data( $user_id, $course_id ) {
1998 if ( get_tutor_option( 'enable_course_review' ) ) {
1999 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2000 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2001 $meta_key = User::get_review_popup_meta( $course_id );
2002 add_user_meta( $user_id, $meta_key, $course_id, true );
2003 }
2004 }
2005 }
2006
2007 /**
2008 * Popup review form on course details
2009 *
2010 * @since 1.0.0
2011 * @return void
2012 */
2013 public function popup_review_form() {
2014 if ( is_user_logged_in() ) {
2015 $user_id = get_current_user_id();
2016 $course_id = get_the_ID();
2017 $meta_key = User::get_review_popup_meta( $course_id );
2018 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2019
2020 if ( is_single() && $course_id === $review_course_id ) {
2021 include tutor()->path . 'views/modal/review.php';
2022 }
2023 }
2024 }
2025
2026 /**
2027 * Review popup data clear
2028 *
2029 * @since 2.4.0
2030 *
2031 * @return void
2032 */
2033 public function clear_review_popup_data() {
2034 tutils()->checking_nonce();
2035
2036 if ( is_user_logged_in() ) {
2037 $user_id = get_current_user_id();
2038 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2039
2040 if ( $course_id ) {
2041 $meta_key = User::get_review_popup_meta( $course_id );
2042 delete_user_meta( $user_id, $meta_key, $course_id );
2043 }
2044
2045 wp_send_json_success();
2046 }
2047 }
2048
2049 /**
2050 * Delete course delete from frontend dashboard
2051 *
2052 * @since 2.0.0
2053 * @return void
2054 */
2055 public function tutor_delete_dashboard_course() {
2056 tutor_utils()->checking_nonce();
2057
2058 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2059 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2060 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2061 }
2062
2063 /**
2064 * Co-instructor can not delete a course
2065 *
2066 * @since 2.1.6
2067 */
2068 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2069 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2070 }
2071
2072 // Check if user is only an instructor.
2073 if ( ! current_user_can( 'administrator' ) ) {
2074 // Check if instructor can trash course.
2075 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2076
2077 if ( ! $can_trash_post ) {
2078 wp_send_json_error( tutor_utils()->error_message() );
2079 }
2080 }
2081
2082 $trash_course = wp_update_post(
2083 array(
2084 'ID' => $course_id,
2085 'post_status' => 'trash',
2086 )
2087 );
2088
2089 if ( $trash_course ) {
2090 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2091 }
2092 wp_send_json_success();
2093 }
2094
2095 /**
2096 * Main author change from gutenberg editor
2097 *
2098 * @since 2.0.0
2099 *
2100 * @param array $data data.
2101 * @param array $postarr post array.
2102 *
2103 * @return mixed
2104 */
2105 public function tutor_add_gutenberg_author( $data, $postarr ) {
2106 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2107 $post_type = $postarr['post_type'];
2108 $courses_post_type = tutor()->course_post_type;
2109
2110 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2111 return $data;
2112 }
2113
2114 /**
2115 * Only admin can change main author
2116 */
2117 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2118 global $wpdb;
2119 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2120 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2121
2122 if ( $post_author > 0 ) {
2123 $data['post_author'] = $post_author;
2124 } else {
2125 $data['post_author'] = get_current_user_id();
2126 }
2127 }
2128
2129 return $data;
2130 }
2131
2132
2133 /**
2134 * Attach product with course when course save from frontend or backend.
2135 *
2136 * @since 1.3.4
2137 *
2138 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2139 *
2140 * @param integer $post_ID course ID.
2141 * @param array $post_data created course post details.
2142 *
2143 * @return void
2144 */
2145 public function attach_product_with_course( $post_ID, $post_data ) {
2146
2147 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2148 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2149
2150 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2151 return;
2152 }
2153
2154 /**
2155 * Unlink product from course.
2156 */
2157 if ( -1 === $product_id ) {
2158 delete_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META );
2159 return;
2160 }
2161
2162 /**
2163 * Free user can only select product from dropdown
2164 */
2165 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2166 if ( $product_id > 0 ) {
2167 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2168 }
2169
2170 return;
2171 }
2172
2173 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2174 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2175 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2176
2177 if ( ! $course_price || $sale_price >= $course_price ) {
2178 return;
2179 }
2180
2181 $course = get_post( $post_ID );
2182
2183 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2184
2185 if ( 'wc' === $monetize_by ) {
2186
2187 $is_update = ( $attached_product_id && wc_get_product( $attached_product_id ) ) ? true : false;
2188
2189 if ( $is_update ) {
2190 $attached_product_id = $product_id;
2191 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2192
2193 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $attached_product_id );
2194 $product_obj = wc_get_product( $product_id );
2195 if ( $product_obj->is_type( 'subscription' ) ) {
2196 update_post_meta( $attached_product_id, '_subscription_price', $course_price );
2197 }
2198
2199 // Set course regular & sale price.
2200 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2201 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2202 } else {
2203 // Create new WC product name with course title.
2204 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2205 if ( $product_id ) {
2206 $product_obj = wc_get_product( $product_id );
2207 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2208 // Mark product for woocommerce.
2209 update_post_meta( $product_id, '_virtual', 'yes' );
2210 update_post_meta( $product_id, '_tutor_product', 'yes' );
2211
2212 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2213 if ( $course_post_thumbnail ) {
2214 set_post_thumbnail( $product_id, $course_post_thumbnail );
2215 }
2216
2217 // Set course regular & sale price.
2218 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2219 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2220 }
2221 }
2222 } elseif ( 'edd' === $monetize_by ) {
2223
2224 $is_update = false;
2225
2226 if ( $attached_product_id ) {
2227 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2228 if ( $edd_price ) {
2229 $is_update = true;
2230 }
2231 }
2232
2233 if ( $is_update ) {
2234 // Update the product.
2235 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2236 } else {
2237 // Create new product.
2238
2239 $post_arr = array(
2240 'post_type' => 'download',
2241 'post_title' => $course->post_title,
2242 'post_status' => 'publish',
2243 'post_author' => get_current_user_id(),
2244 );
2245 $download_id = wp_insert_post( $post_arr );
2246 if ( $download_id ) {
2247 // EDD edd_price.
2248 update_post_meta( $download_id, 'edd_price', $course_price );
2249
2250 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2251 // Mark product for EDD.
2252 update_post_meta( $download_id, '_tutor_product', 'yes' );
2253
2254 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2255 if ( $course_post_thumbnail ) {
2256 set_post_thumbnail( $download_id, $course_post_thumbnail );
2257 }
2258 }
2259 }
2260 }
2261 }
2262
2263 /**
2264 * Add Course level to course settings
2265 *
2266 * @since 1.4.1
2267 *
2268 * @param array $args arguments.
2269 * @return array
2270 */
2271 public function add_course_level_to_settings( $args ) {
2272 $course_id = get_the_ID();
2273 $levels = tutor_utils()->course_levels();
2274 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2275
2276 $args['general']['fields']['_tutor_course_level'] = array(
2277 'type' => 'select',
2278 'label' => __( 'Difficulty Level', 'tutor' ),
2279 'label_title' => __( 'Enable', 'tutor' ),
2280 'options' => $levels,
2281 'value' => $course_level ? $course_level : 'intermediate',
2282 'desc' => __( 'Course difficulty level', 'tutor' ),
2283 );
2284
2285 return $args;
2286 }
2287
2288 /**
2289 * Check if course starting
2290 *
2291 * @since 1.4.8
2292 * @return void
2293 */
2294 public function tutor_lesson_load_before() {
2295 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2296 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2297 if ( is_user_logged_in() ) {
2298 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2299 if ( ! $completed_lessons && ! $is_course_started ) {
2300 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2301 do_action( 'tutor/course/started', $course_id );
2302 }
2303 }
2304 }
2305
2306 /**
2307 * Add Course level to course settings
2308 *
2309 * @since 1.4.8
2310 * @return void
2311 */
2312 public function course_elements_enable_disable() {
2313 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2314 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2315 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2316 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2317 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2318 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2319 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2320 }
2321
2322 /**
2323 * Enable disable course progress bar
2324 *
2325 * @since 1.4.8
2326 *
2327 * @param string $html HTML string.
2328 * @return string
2329 */
2330 public function enable_disable_course_progress_bar( $html ) {
2331 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2332 if ( $disable_option ) {
2333 return '';
2334 }
2335 return $html;
2336 }
2337
2338 /**
2339 * Enable disable material includes
2340 *
2341 * @since 1.4.8
2342 *
2343 * @param string $html HTML string.
2344 * @return string
2345 */
2346 public function enable_disable_material_includes( $html ) {
2347 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2348 if ( $disable_option ) {
2349 return '';
2350 }
2351 return $html;
2352 }
2353
2354 /**
2355 * Enable disable course content
2356 *
2357 * @since 1.4.8
2358 *
2359 * @param string $html HTML string.
2360 * @return string
2361 */
2362 public function enable_disable_course_content( $html ) {
2363 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2364 if ( $disable_option ) {
2365 return '';
2366 }
2367 return $html;
2368 }
2369
2370 /**
2371 * Enable disable course benefits
2372 *
2373 * @since 1.4.8
2374 *
2375 * @param string $html HTML string.
2376 * @return string
2377 */
2378 public function enable_disable_course_benefits( $html ) {
2379 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2380 if ( $disable_option ) {
2381 return '';
2382 }
2383 return $html;
2384 }
2385
2386 /**
2387 * Enable disable course requirements
2388 *
2389 * @since 1.4.8
2390 *
2391 * @param string $html HTML string.
2392 * @return string
2393 */
2394 public function enable_disable_course_requirements( $html ) {
2395 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2396 if ( $disable_option ) {
2397 return '';
2398 }
2399 return $html;
2400 }
2401
2402 /**
2403 * Enable disable course target audience
2404 *
2405 * @since 1.4.8
2406 *
2407 * @param string $html HTML string.
2408 * @return string
2409 */
2410 public function enable_disable_course_target_audience( $html ) {
2411 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2412 if ( $disable_option ) {
2413 return '';
2414 }
2415 return $html;
2416 }
2417
2418 /**
2419 * Enable disable course nav items
2420 *
2421 * @since 1.4.8
2422 *
2423 * @param array $items item list.
2424 * @param integer $course_id course ID.
2425 *
2426 * @return array
2427 */
2428 public function enable_disable_course_nav_items( $items, $course_id ) {
2429 global $wp_query, $post;
2430 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2431 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2432 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2433
2434 // Whether Q&A enabled.
2435 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2436 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2437 unset( $items['questions'] );
2438 }
2439 }
2440
2441 // Whether announcment enabled.
2442 if ( $disable_course_announcements ) {
2443 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2444 unset( $items['announcements'] );
2445 }
2446 }
2447
2448 // Hide review section if disabled.
2449 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2450 unset( $items['reviews'] );
2451 }
2452
2453 // Whether enrollment require.
2454 $is_enrolled = tutor_utils()->is_enrolled();
2455
2456 return array_filter(
2457 $items,
2458 function ( $item ) use ( $is_enrolled ) {
2459 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2460 return $is_enrolled;
2461 }
2462 return true;
2463 }
2464 );
2465 }
2466
2467 /**
2468 * Filter product in shop page
2469 *
2470 * @since 1.4.9
2471 * @return void|null
2472 */
2473 public function filter_product_in_shop_page() {
2474 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2475 if ( ! $hide_course_from_shop_page ) {
2476 return;
2477 }
2478 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2479 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2480 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2481 }
2482
2483
2484 /**
2485 * Tutor product meta query
2486 *
2487 * @since 1.4.9
2488 * @return array
2489 */
2490 public function tutor_product_meta_query() {
2491 $meta_query = array(
2492 'key' => '_tutor_product',
2493 'compare' => 'NOT EXISTS',
2494 );
2495 return $meta_query;
2496 }
2497
2498 /**
2499 * Filter product in woocommerce shop page
2500 *
2501 * @since 1.4.9
2502 *
2503 * @param \WP_Query $wp_query WP Query instance.
2504 * @return \WP_Query
2505 */
2506 public function filter_woocommerce_product_query( $wp_query ) {
2507 $product_ids = $this->get_connected_wc_product_ids();
2508 $wp_query->set( 'post__not_in', $product_ids );
2509 return $wp_query;
2510 }
2511
2512 /**
2513 * Get connected woocommerce product ids for course and course bundle
2514 *
2515 * @since 2.7.2
2516 *
2517 * @return array
2518 */
2519 public function get_connected_wc_product_ids() {
2520 global $wpdb;
2521
2522 $results = $wpdb->get_results(
2523 $wpdb->prepare(
2524 "SELECT DISTINCT pm.meta_value product_id
2525 FROM {$wpdb->posts} p
2526 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2527 AND pm.meta_key = %s
2528 WHERE post_type IN( 'courses','course-bundle' )",
2529 '_tutor_course_product_id'
2530 )
2531 );
2532
2533 $ids = array();
2534 if ( is_array( $results ) && count( $results ) ) {
2535 $ids = array_column( $results, 'product_id' );
2536 }
2537
2538 return $ids;
2539 }
2540
2541 /**
2542 * Filter product in edd downloads shortcode page
2543 *
2544 * @since 1.4.9
2545 *
2546 * @param \WP_Query $query WP Query instance.
2547 * @return \WP_Query
2548 */
2549 public function filter_edd_downloads_query( $query ) {
2550 $query['meta_query'][] = $this->tutor_product_meta_query();
2551 return $query;
2552 }
2553
2554 /**
2555 * Filter product in edd downloads archive page
2556 *
2557 * @since 1.4.9
2558 *
2559 * @param \WP_Query $wp_query WP Query instance.
2560 * @return \WP_Query
2561 */
2562 public function filter_archive_meta_query( $wp_query ) {
2563 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2564 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2565 }
2566 return $wp_query;
2567 }
2568
2569 /**
2570 * Removed course price if already enrolled at single course
2571 *
2572 * @since 1.5.8
2573 *
2574 * @param string $html HTML string.
2575 * @return string
2576 */
2577 public function remove_price_if_enrolled( $html ) {
2578 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2579
2580 if ( $should_removed ) {
2581 $course_id = get_the_ID();
2582 $enrolled = tutor_utils()->is_enrolled( $course_id );
2583 if ( $enrolled ) {
2584 $html = '';
2585 }
2586 }
2587 return $html;
2588 }
2589
2590 /**
2591 * Check if all lessons and quizzes done before mark course complete.
2592 *
2593 * @since 1.5.8
2594 *
2595 * @param string $html HTML string.
2596 * @return string
2597 */
2598 public function tutor_lms_hide_course_complete_btn( $html ) {
2599
2600 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2601 if ( 'strict' !== $completion_mode ) {
2602 return $html;
2603 }
2604
2605 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2606 $lesson_count = tutor_utils()->get_lesson_count_by_course();
2607
2608 if ( $completed_lesson < $lesson_count ) {
2609 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2610 <div class="tutor-alert-text">
2611 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2612 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2613 </div>
2614 </div>';
2615 }
2616
2617 $quizzes = array();
2618 $assignments = array();
2619
2620 $course_contents = tutor_utils()->get_course_contents_by_id();
2621 if ( tutor_utils()->count( $course_contents ) ) {
2622 foreach ( $course_contents as $content ) {
2623 if ( 'tutor_quiz' === $content->post_type ) {
2624 $quizzes[] = $content;
2625 }
2626 if ( 'tutor_assignments' === $content->post_type ) {
2627 $assignments[] = $content;
2628 }
2629 }
2630 }
2631
2632 $required_assignment_pass = 0;
2633
2634 foreach ( $assignments as $row ) {
2635
2636 $submitted_assignment = tutor_utils()->is_assignment_submitted( $row->ID );
2637 $is_reviewed_by_instructor = null === $submitted_assignment
2638 ? false
2639 : get_comment_meta( $submitted_assignment->comment_ID, 'evaluate_time', true );
2640
2641 if ( $submitted_assignment && $is_reviewed_by_instructor ) {
2642 $pass_mark = tutor_utils()->get_assignment_option( $submitted_assignment->comment_post_ID, 'pass_mark' );
2643 $given_mark = get_comment_meta( $submitted_assignment->comment_ID, 'assignment_mark', true );
2644
2645 if ( $given_mark < $pass_mark ) {
2646 $required_assignment_pass++;
2647 }
2648 } else {
2649 $required_assignment_pass++;
2650 }
2651 }
2652
2653 $is_quiz_pass = true;
2654 $required_quiz_pass = 0;
2655
2656 if ( tutor_utils()->count( $quizzes ) ) {
2657 foreach ( $quizzes as $quiz ) {
2658
2659 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2660 if ( $attempt ) {
2661 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2662 $earned_percentage = $attempt->earned_marks > 0 ? ( number_format( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) ) : 0;
2663
2664 if ( $earned_percentage < $passing_grade ) {
2665 $required_quiz_pass++;
2666 $is_quiz_pass = false;
2667 }
2668 } else {
2669 $required_quiz_pass++;
2670 $is_quiz_pass = false;
2671 }
2672 }
2673 }
2674
2675 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2676 $_msg = '';
2677 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2678 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2679
2680 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2681 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2682 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2683 }
2684
2685 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2686 //phpcs:ignore
2687 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2688 }
2689
2690 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2691 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2692 $_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 );
2693 }
2694
2695 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2696 <div class="tutor-alert-text">
2697 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2698 <span>' . $_msg . '</span>
2699 </div>
2700 </div>';
2701 }
2702
2703 return $html;
2704 }
2705
2706 /**
2707 * Generate Gradebook
2708 *
2709 * @since 1.5.8
2710 *
2711 * @param string $html HTML string.
2712 * @return string
2713 */
2714 public function get_generate_greadbook( $html ) {
2715 if ( ! tutor_utils()->is_completed_course() ) {
2716 return '';
2717 }
2718 return $html;
2719 }
2720
2721 /**
2722 * Add social share content in header
2723 *
2724 * @since 1.6.3
2725 * @return void
2726 */
2727 public function social_share_content() {
2728 global $wp_query, $post;
2729 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2730 <!--Facebook-->
2731 <meta property="og:type" content="website"/>
2732 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2733 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2734 <!--Twitter-->
2735 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2736 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2737 <!--Google+-->
2738 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2739 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2740 <?php
2741 }
2742 }
2743
2744 /**
2745 * Delete associated enrollment
2746 *
2747 * @since 1.8.2
2748 *
2749 * @param integer $post_id post ID.
2750 * @return void
2751 */
2752 public function delete_associated_enrollment( $post_id ) {
2753 global $wpdb;
2754
2755 $enroll_id = $wpdb->get_var(
2756 $wpdb->prepare(
2757 "SELECT
2758 post_id
2759 FROM
2760 {$wpdb->postmeta}
2761 WHERE
2762 meta_key='_tutor_enrolled_by_order_id'
2763 AND meta_value = %d
2764 ",
2765 $post_id
2766 )
2767 );
2768
2769 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2770
2771 $course_id = get_post_field( 'post_parent', $enroll_id );
2772 $user_id = get_post_field( 'post_author', $enroll_id );
2773
2774 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2775 }
2776 }
2777
2778 /**
2779 * Reset course progress.
2780 *
2781 * @since 1.5.8
2782 * @return void
2783 */
2784 public function tutor_reset_course_progress() {
2785 tutor_utils()->checking_nonce();
2786 $course_id = Input::post( 'course_id' );
2787
2788 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2789 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2790 return;
2791 }
2792
2793 tutor_utils()->delete_course_progress( $course_id );
2794 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2795 }
2796
2797 /**
2798 * Do enroll if guest attempt to enroll and course is free
2799 *
2800 * @since 1.9.8
2801 *
2802 * @param integer $course_id course ID.
2803 * @param integer $user_id user ID.
2804
2805 * @return void
2806 */
2807 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
2808 $course_id = sanitize_text_field( $course_id );
2809 if ( $course_id ) {
2810 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2811 if ( ! $is_purchasable ) {
2812 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
2813 do_action( 'guest_attempt_after_enrollment', $course_id );
2814 }
2815 }
2816 }
2817
2818 /**
2819 * Handle course enrollment
2820 *
2821 * @since 2.1.0
2822 * @return void
2823 */
2824 public function course_enrollment() {
2825 tutor_utils()->checking_nonce();
2826
2827 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2828 $user_id = get_current_user_id();
2829
2830 if ( $course_id ) {
2831 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
2832 if ( $enroll ) {
2833 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
2834 } else {
2835 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
2836 }
2837 } else {
2838 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
2839 }
2840 }
2841
2842 /**
2843 * After trash a course direct to the course list page
2844 *
2845 * @since 2.1.7
2846 *
2847 * @param integer $post_id int course id.
2848 *
2849 * @return void
2850 */
2851 public static function redirect_to_course_list_page( int $post_id ): void {
2852 $post = get_post( $post_id );
2853 if ( tutor()->course_post_type === $post->post_type ) {
2854 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2855 if ( ! $is_gutenberg_enabled ) {
2856 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
2857 exit;
2858 }
2859 }
2860 }
2861
2862 /**
2863 * Create or update WooCommerce product
2864 *
2865 * If product id not set it will create new one.
2866 *
2867 * @since 2.2.0
2868 *
2869 * @param string $title product title.
2870 * @param string $reg_price product price.
2871 * @param string $sale_price product sale price.
2872 * @param int $product_id product ID.
2873 * @param string $status product status.
2874 *
2875 * @return integer Product id or return 0 if WC not exists
2876 */
2877 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
2878 if ( ! tutor_utils()->has_wc() ) {
2879 return 0;
2880 }
2881
2882 $product_obj = new \WC_Product();
2883 if ( $product_id ) {
2884 $product_obj = wc_get_product( $product_id );
2885 }
2886
2887 $product_obj->set_name( $title );
2888 $product_obj->set_status( $status );
2889 $product_obj->set_price( $reg_price );
2890 $product_obj->set_regular_price( $reg_price );
2891
2892 if ( $sale_price > 0 ) {
2893 $product_obj->set_sale_price( $sale_price );
2894 } else {
2895 $product_obj->set_sale_price( null );
2896 }
2897
2898 $product_obj->set_sold_individually( true );
2899
2900 return $product_obj->save();
2901 }
2902
2903 /**
2904 * Load media scripts
2905 *
2906 * @since 3.0.0
2907 *
2908 * @return void
2909 */
2910 public static function load_media_scripts() {
2911 // Add style on the head tag.
2912 $screen_reader_text_style = '
2913 .screen-reader-text
2914 {
2915 position: absolute;
2916 top: -10000em;
2917 width: 1px;
2918 height: 1px;
2919 margin: -1px;
2920 padding: 0;
2921 overflow: hidden;
2922 clip: rect(0,0,0,0);
2923 border: 0;
2924 }
2925 ';
2926
2927 wp_add_inline_style(
2928 'media-views',
2929 $screen_reader_text_style
2930 );
2931
2932 add_action(
2933 'wp_print_footer_scripts',
2934 function () {
2935 if ( function_exists( 'wp_print_media_templates' ) ) {
2936 wp_print_media_templates();
2937 }
2938 }
2939 );
2940 }
2941
2942 /**
2943 * Get course/bundle mini info
2944 *
2945 * @since 3.0.0
2946 *
2947 * @param object $post Course or bundle post.
2948 *
2949 * @return array
2950 */
2951 public static function get_mini_info( object $post ) {
2952 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
2953 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
2954 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
2955 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
2956
2957 $info = array(
2958 'id' => $post->ID,
2959 'title' => $post->post_title,
2960 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
2961 'is_purchasable' => $is_purchasable,
2962 'regular_price' => $regular_price,
2963 'sale_price' => $sale_price,
2964 );
2965
2966 if ( 'course-bundle' === $post->post_type && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
2967 $info['total_course'] = count( BundleModel::get_bundle_course_ids( $post->ID ) );
2968 }
2969
2970 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
2971
2972 return $card_data;
2973 }
2974
2975 /**
2976 * Get course/bundle card data
2977 *
2978 * This method will return all data that contain in
2979 * course card
2980 *
2981 * @since 3.0.0
2982 *
2983 * @param object $post Course or bundle post.
2984 *
2985 * @return array
2986 */
2987 public static function get_card_data( object $post ) {
2988 $info = self::get_mini_info( $post );
2989
2990 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
2991 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
2992 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
2993
2994 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
2995
2996 return $card_data;
2997 }
2998
2999 /**
3000 * Filter user list access for instructor
3001 *
3002 * @since 3.0.0
3003 *
3004 * @param bool $access access.
3005 *
3006 * @return bool
3007 */
3008 public function user_list_access_for_instructor( $access ) {
3009 $is_instructor = User::is_instructor();
3010 return $access || $is_instructor;
3011 }
3012
3013 /**
3014 * Filter user list args for instructor
3015 *
3016 * @since 3.0.0
3017 *
3018 * @param array $args args.
3019 *
3020 * @return array
3021 */
3022 public function user_list_args_for_instructor( $args ) {
3023 if ( User::is_instructor() ) {
3024 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3025 unset( $args['fields']['user_email'] );
3026 }
3027 }
3028
3029 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3030 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3031 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3032 }
3033
3034 return $args;
3035 }
3036
3037 }
3038