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