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