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