PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.7.3
Tutor LMS – eLearning and online course solution v2.7.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 2 years ago Admin.php 2 years ago Ajax.php 2 years ago Announcements.php 3 years ago Assets.php 2 years ago Backend_Page_Trait.php 3 years 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 3 years ago Course_Widget.php 3 years ago Custom_Validation.php 3 years ago Dashboard.php 3 years ago FormHandler.php 2 years ago Frontend.php 2 years ago Gutenberg.php 3 years ago Input.php 3 years ago Instructor.php 2 years ago Instructors_List.php 1 year ago Lesson.php 2 years ago Options_V2.php 1 year ago Permalink.php 2 years ago Post_types.php 2 years ago Private_Course_Access.php 3 years ago Q_And_A.php 1 year ago Question_Answers_List.php 3 years ago Quiz.php 2 years ago Quiz_Attempts_List.php 2 years ago RestAPI.php 2 years ago Reviews.php 3 years ago Rewrite_Rules.php 2 years ago Shortcode.php 1 year ago Student.php 2 years ago Students_List.php 3 years ago Taxonomies.php 3 years ago Template.php 2 years ago Theme_Compatibility.php 3 years ago Tools.php 3 years ago Tools_V2.php 2 years ago Tutor.php 2 years ago TutorEDD.php 2 years ago Tutor_Base.php 2 years ago Tutor_Setup.php 2 years ago Upgrader.php 2 years ago User.php 2 years ago Utils.php 1 year ago Video_Stream.php 3 years ago WhatsNew.php 2 years ago Withdraw.php 2 years ago Withdraw_Requests_List.php 3 years ago WooCommerce.php 2 years ago
Course.php
1800 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 TUTOR\Input;
18 use Tutor\Models\CourseModel;
19
20 /**
21 * Course Class
22 *
23 * @since 1.0.0
24 */
25 class Course extends Tutor_Base {
26
27 /**
28 * Additional course meta info
29 *
30 * @var array
31 */
32 private $additional_meta = array(
33 '_tutor_enable_qa',
34 '_tutor_is_public_course',
35 );
36
37 /**
38 * Constructor
39 *
40 * @since 1.0.0
41 * @return void
42 */
43 public function __construct() {
44 parent::__construct();
45
46 add_action( 'add_meta_boxes', array( $this, 'register_meta_box' ) );
47 add_action( 'save_post_' . $this->course_post_type, array( $this, 'save_course_meta' ), 10, 2 );
48 add_action( 'wp_ajax_tutor_save_topic', array( $this, 'tutor_save_topic' ) );
49
50 /**
51 * Add Column
52 */
53 add_filter( "manage_{$this->course_post_type}_posts_columns", array( $this, 'add_column' ), 10, 1 );
54 add_action( "manage_{$this->course_post_type}_posts_custom_column", array( $this, 'custom_lesson_column' ), 10, 2 );
55
56 add_action( 'wp_ajax_tutor_delete_topic', array( $this, 'tutor_delete_topic' ) );
57
58 /**
59 * Frontend Action
60 */
61 add_action( 'template_redirect', array( $this, 'enroll_now' ) );
62 add_action( 'init', array( $this, 'mark_course_complete' ) );
63
64 /**
65 * Frontend Dashboard
66 */
67 add_action( 'wp_ajax_tutor_delete_dashboard_course', array( $this, 'tutor_delete_dashboard_course' ) );
68
69 /**
70 * Gutenberg author support
71 */
72 add_filter( 'wp_insert_post_data', array( $this, 'tutor_add_gutenberg_author' ), '99', 2 );
73
74 /**
75 * Frontend metabox supports for course builder
76 *
77 * @since v.1.3.4
78 */
79 add_action( 'tutor/dashboard_course_builder_form_field_after', array( $this, 'register_meta_box_in_frontend' ) );
80
81 /**
82 * Do Stuff for the course save from frontend
83 */
84 add_action( 'save_tutor_course', array( $this, 'attach_product_with_course' ), 10, 2 );
85
86 /**
87 * Add course level to course settings
88 *
89 * @since v.1.4.1
90 */
91 add_filter( 'tutor_course_settings_tabs', array( $this, 'add_course_level_to_settings' ) );
92
93 /**
94 * Enable Disable Course Details Page Feature
95 *
96 * @since v.1.4.8
97 */
98 $this->course_elements_enable_disable();
99
100 /**
101 * Check if course starting, set meta if starting
102 *
103 * @since v.1.4.8
104 */
105 add_action( 'tutor_lesson_load_before', array( $this, 'tutor_lesson_load_before' ) );
106
107 /**
108 * Filter product in shop page
109 *
110 * @since v.1.4.9
111 */
112 $this->filter_product_in_shop_page();
113
114 /**
115 * Remove the course price if enrolled
116 *
117 * @since 1.5.8
118 */
119 add_filter( 'tutor_course_price', array( $this, 'remove_price_if_enrolled' ) );
120
121 /**
122 * Remove course complete button if course completion is strict mode
123 *
124 * @since v.1.6.1
125 */
126 add_filter( 'tutor_course/single/complete_form', array( $this, 'tutor_lms_hide_course_complete_btn' ) );
127 add_filter( 'get_gradebook_generate_form_html', array( $this, 'get_generate_greadbook' ) );
128
129 /**
130 * Add social share content in header
131 *
132 * @since v.1.6.3
133 */
134 add_action( 'wp_head', array( $this, 'social_share_content' ) );
135
136 /**
137 * Delete course data after deleted course
138 *
139 * @since v.1.6.6
140 */
141 add_action( 'deleted_post', array( new CourseModel(), 'delete_course_data' ) );
142
143 /**
144 * Delete course data after deleted course
145 *
146 * @since v.1.8.2
147 */
148 add_action( 'before_delete_post', array( $this, 'delete_associated_enrollment' ) );
149
150 /**
151 * Show only own uploads in media library if user is instructor
152 *
153 * @since v1.8.9
154 */
155 add_filter( 'posts_where', array( $this, 'restrict_media' ) );
156
157 /**
158 * Restrict new enrol/purchase button if course member limit reached
159 *
160 * @since v1.9.0
161 */
162 add_filter( 'tutor_course_restrict_new_entry', array( $this, 'restrict_new_student_entry' ) );
163
164 /**
165 * Reset course progress on retake
166 *
167 * @since v1.9.5
168 */
169 add_action( 'wp_ajax_tutor_reset_course_progress', array( $this, 'tutor_reset_course_progress' ) );
170
171 /**
172 * Popup for review
173 *
174 * @since v1.9.7
175 */
176 add_action( 'wp_footer', array( $this, 'popup_review_form' ) );
177 add_action( 'wp_ajax_tutor_clear_review_popup_data', array( $this, 'clear_review_popup_data' ) );
178
179 /**
180 * Do enroll after login if guest take enroll attempt
181 *
182 * @since 1.9.8
183 */
184 add_action( 'tutor_do_enroll_after_login_if_attempt', array( $this, 'enroll_after_login_if_attempt' ), 10, 2 );
185
186 add_action( 'wp_ajax_tutor_update_course_content_order', array( $this, 'tutor_update_course_content_order' ) );
187
188 add_action( 'wp_ajax_tutor_get_wc_product', array( $this, 'tutor_get_wc_product' ) );
189
190 add_action( 'wp_ajax_tutor_course_enrollment', array( $this, 'course_enrollment' ) );
191
192 /**
193 * After trash a course redirect to course list page
194 *
195 * @since 2.1.7
196 */
197 add_action( 'trashed_post', __CLASS__ . '::redirect_to_course_list_page' );
198
199 add_filter( 'tutor_enroll_required_login_class', array( $this, 'add_enroll_required_login_class' ) );
200 /**
201 * Remove wp trash button if instructor settings is disabled
202 *
203 * @since 2.7.3
204 */
205 add_action( 'admin_init', array( $this, 'disable_course_trash_instructor' ) );
206 }
207
208 /**
209 * Remove move to trash button on WordPress editor for instructor.
210 *
211 * @since 2.7.3
212 *
213 * @return void
214 */
215 public function disable_course_trash_instructor() {
216 if ( current_user_can( 'edit_tutor_course' ) && ! current_user_can( 'administrator' ) ) {
217 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
218 $role = get_role( tutor()->instructor_role );
219
220 if ( ! $can_trash_post ) {
221 $role->remove_cap( 'delete_tutor_courses' );
222 $role->remove_cap( 'delete_tutor_course' );
223 } else {
224 $role->add_cap( 'delete_tutor_courses' );
225 $role->add_cap( 'delete_tutor_course' );
226 }
227 }
228 }
229
230 /**
231 * Add enroll require login class
232 *
233 * @since 2.6.0
234 *
235 * @param string $class_name css class name.
236 *
237 * @return string
238 */
239 public function add_enroll_required_login_class( $class_name ) {
240 $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
241 if ( ! $enabled_tutor_login ) {
242 return '';
243 }
244
245 return $class_name;
246 }
247
248 /**
249 * Get course associate WC product info by Ajax request
250 *
251 * @since 2.0.7
252 * @return void
253 */
254 public function tutor_get_wc_product() {
255 tutor_utils()->checking_nonce();
256 $product_id = Input::post( 'product_id' );
257 $product = wc_get_product( $product_id );
258 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
259
260 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
261 /**
262 * If selected product is already linked with
263 * a course & it is not the current course the
264 * return error
265 *
266 * @since v2.1.0
267 */
268 if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
269 wp_send_json_error(
270 __( 'One product can not be added to multiple course!', 'tutor' )
271 );
272 }
273
274 if ( $product ) {
275 $data = array(
276 'name' => $product->get_name(),
277 'regular_price' => $product->get_regular_price(),
278 'sale_price' => $product->get_sale_price(),
279 );
280 wp_send_json_success( $data );
281 } else {
282 wp_send_json_error( __( 'Product not found', 'tutor' ) );
283 }
284 }
285
286 /**
287 * Update course content order
288 *
289 * @since 1.0.0
290 * @return void
291 */
292 public function tutor_update_course_content_order() {
293 tutor_utils()->checking_nonce();
294
295 if ( Input::has( 'content_parent' ) ) {
296 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
297 $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
298 $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
299
300 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
301 wp_send_json_success( array( 'message' => __( 'Access Denied!', 'tutor' ) ) );
302 exit;
303 }
304
305 // Update the parent topic id of the content.
306 global $wpdb;
307 $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
308 }
309
310 // Save course content order.
311 $this->save_course_content_order();
312
313 wp_send_json_success();
314 }
315
316 /**
317 * Restrict new student entry
318 *
319 * @since 1.0.0
320 * @param mixed $content content.
321 * @return mixed
322 */
323 public function restrict_new_student_entry( $content ) {
324
325 if ( ! tutor_utils()->is_course_fully_booked() ) {
326 // No restriction if not fully booked.
327 return $content;
328 }
329
330 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">' .
331 __( 'Fully Booked', 'tutor' )
332 . '</div></div>';
333 }
334
335 /**
336 * Restrict media
337 *
338 * @since 1.0.0
339 * @param string $where where clause.
340 * @return string
341 */
342 public function restrict_media( $where ) {
343 $action = Input::post( 'action' );
344 if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
345 if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
346 $where .= ' AND post_author=' . get_current_user_id();
347 }
348 }
349
350 return $where;
351 }
352
353 /**
354 * Registering metabox
355 *
356 * @since 1.0.0
357 * @return void
358 */
359 public function register_meta_box() {
360 $course_post_type = tutor()->course_post_type;
361
362 tutor_meta_box_wrapper( 'tutor-course-topics', __( 'Course Builder', 'tutor' ), array( $this, 'course_meta_box' ), $course_post_type, 'advanced', 'default', 'tutor-admin-post-meta' );
363
364 tutor_meta_box_wrapper( 'tutor-course-additional-data', __( 'Additional Data', 'tutor' ), array( $this, 'course_additional_data_meta_box' ), $course_post_type, 'advanced', 'default', 'tutor-admin-post-meta' );
365
366 tutor_meta_box_wrapper( 'tutor-course-videos', __( 'Video', 'tutor' ), array( $this, 'video_metabox' ), $course_post_type, 'advanced', 'default', 'tutor-admin-post-meta' );
367 }
368
369 /**
370 * Course meta box (Topics)
371 *
372 * @since 1.0.0
373 * @param boolean $echo display or not.
374 * @return string
375 */
376 public function course_meta_box( $echo = true ) {
377 $file_path = tutor()->path . 'views/metabox/course-topics.php';
378
379 if ( $echo ) {
380 /**
381 * Use echo raise WPCS security issue
382 * Helper wp_kses_post break content.
383 */
384 include $file_path;
385 } else {
386 ob_start();
387 include $file_path;
388 return ob_get_clean();
389 }
390 }
391
392 /**
393 * Additional data meta box
394 *
395 * @since 1.0.0
396 * @param boolean $echo print data or return.
397 * @return string
398 */
399 public function course_additional_data_meta_box( $echo = true ) {
400 $file_path = tutor()->path . 'views/metabox/course-additional-data.php';
401
402 if ( $echo ) {
403 /**
404 * Use echo raise WPCS security issue
405 * Helper wp_kses_post break content.
406 */
407 include $file_path;
408 } else {
409 ob_start();
410 include $file_path;
411 return ob_get_clean();
412 }
413 }
414
415 /**
416 * Video meta box
417 *
418 * @since 1.0.0
419 * @param boolean $echo print data or return.
420 * @return string
421 */
422 public function video_metabox( $echo = true ) {
423 $file_path = tutor()->path . 'views/metabox/video-metabox.php';
424
425 if ( $echo ) {
426 /**
427 * Use echo raise WPCS security issue
428 * Helper wp_kses_post break content.
429 */
430 include $file_path;
431 } else {
432 ob_start();
433 include $file_path;
434 return ob_get_clean();
435 }
436 }
437
438 /**
439 * Register metabox in course builder tutor
440 *
441 * @since 1.3.4
442 * @return void
443 */
444 public function register_meta_box_in_frontend() {
445 global $post;
446
447 do_action( 'tutor_course_builder_metabox_before', get_the_ID() );
448
449 course_builder_section_wrap( $this->video_metabox( false ), __( 'Video', 'tutor' ) );
450 do_action( 'tutor/frontend_course_edit/after/video', $post );
451
452 course_builder_section_wrap( $this->course_meta_box( false ), __( 'Course Builder', 'tutor' ) );
453 do_action( 'tutor/frontend_course_edit/after/course_builder', $post );
454
455 course_builder_section_wrap( $this->course_additional_data_meta_box( false ), __( 'Additional Data', 'tutor' ) );
456 do_action( 'tutor/frontend_course_edit/after/additional_data', $post );
457
458 do_action( 'tutor_course_builder_metabox_after', get_the_ID() );
459 }
460
461 /**
462 * Save course content order
463 *
464 * @since 1.0.0
465 * @return void
466 */
467 private function save_course_content_order() {
468 global $wpdb;
469
470 $new_order = Input::post( 'tutor_topics_lessons_sorting' );
471 if ( ! empty( $new_order ) ) {
472 $order = json_decode( $new_order, true );
473
474 if ( is_array( $order ) && count( $order ) ) {
475 $i = 0;
476 foreach ( $order as $topic ) {
477 $i++;
478 $wpdb->update(
479 $wpdb->posts,
480 array( 'menu_order' => $i ),
481 array( 'ID' => $topic['topic_id'] )
482 );
483
484 /**
485 * Removing All lesson with topic
486 */
487
488 $wpdb->update(
489 $wpdb->posts,
490 array( 'post_parent' => 0 ),
491 array( 'post_parent' => $topic['topic_id'] )
492 );
493
494 /**
495 * Lesson Attaching with topic ID
496 * Sorting lesson
497 */
498 if ( isset( $topic['lesson_ids'] ) ) {
499 $lesson_ids = $topic['lesson_ids'];
500 } else {
501 $lesson_ids = array();
502 }
503 if ( count( $lesson_ids ) ) {
504 foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
505 $wpdb->update(
506 $wpdb->posts,
507 array(
508 'post_parent' => $topic['topic_id'],
509 'menu_order' => $lesson_key,
510 ),
511 array( 'ID' => $lesson_id )
512 );
513 }
514 }
515 }
516 }
517 }
518 }
519
520 /**
521 * Insert Topic and attached it with Course
522 *
523 * @since 1.0.0
524 *
525 * @param integer $post_ID post ID.
526 * @param object $post post object.
527 *
528 * @return void
529 */
530 public function save_course_meta( $post_ID, $post ) {
531 global $wpdb;
532
533 do_action( 'tutor_save_course', $post_ID, $post );
534
535 /**
536 * Save course price type
537 */
538 $price_type = Input::post( 'tutor_course_price_type' );
539 if ( $price_type ) {
540 update_post_meta( $post_ID, '_tutor_course_price_type', $price_type );
541 }
542
543 //phpcs:disable WordPress.Security.NonceVerification.Missing
544 // Course Duration.
545 if ( ! empty( $_POST['course_duration'] ) ) {
546 $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
547 update_post_meta( $post_ID, '_course_duration', $video );
548 }
549
550 if ( ! empty( $_POST['_tutor_course_level'] ) ) {
551 $course_level = Input::post( '_tutor_course_level' );
552 update_post_meta( $post_ID, '_tutor_course_level', $course_level );
553 }
554
555 $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
556 if ( $additional_data_edit ) {
557 if ( ! empty( $_POST['course_benefits'] ) ) {
558 $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
559 update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
560 } else {
561 if ( ! tutor_is_rest() ) {
562 delete_post_meta( $post_ID, '_tutor_course_benefits' );
563 }
564 }
565
566 if ( ! empty( $_POST['course_requirements'] ) ) {
567 $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
568 update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
569 } else {
570 if ( ! tutor_is_rest() ) {
571 delete_post_meta( $post_ID, '_tutor_course_requirements' );
572 }
573 }
574
575 if ( ! empty( $_POST['course_target_audience'] ) ) {
576 $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
577 update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
578 } else {
579 if ( ! tutor_is_rest() ) {
580 delete_post_meta( $post_ID, '_tutor_course_target_audience' );
581 }
582 }
583
584 if ( ! empty( $_POST['course_material_includes'] ) ) {
585 $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
586 update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
587 } else {
588 if ( ! tutor_is_rest() ) {
589 delete_post_meta( $post_ID, '_tutor_course_material_includes' );
590 }
591 }
592 //phpcs:enable WordPress.Security.NonceVerification.Missing
593 }
594
595 /**
596 * Sorting Topics and lesson
597 */
598 $this->save_course_content_order();
599
600 // Additional data like course intro video.
601 if ( $additional_data_edit ) {
602 // Sanitize data through helper method.
603 $video = Input::sanitize_array(
604 $_POST['video'] ?? array(), //phpcs:ignore
605 array(
606 'source_external_url' => 'esc_url',
607 'source_embedded' => 'wp_kses_post',
608 ),
609 true
610 );
611 $video_source = tutor_utils()->array_get( 'source', $video );
612 if ( -1 !== $video_source ) {
613 update_post_meta( $post_ID, '_video', $video );
614 } else {
615 if ( ! tutor_is_rest() ) {
616 delete_post_meta( $post_ID, '_video' );
617 }
618 }
619 }
620
621 /**
622 * Adding author to instructor automatically
623 */
624
625 // Override post author id.
626 $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
627 $attached = (int) $wpdb->get_var(
628 $wpdb->prepare(
629 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
630 WHERE user_id = %d
631 AND meta_key = '_tutor_instructor_course_id'
632 AND meta_value = %d ",
633 $author_id,
634 $post_ID
635 )
636 );
637
638 if ( ! $attached ) {
639 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
640 }
641
642 /**
643 * Disable question and answer for this course
644 *
645 * @since 1.7.0
646 */
647 if ( $additional_data_edit ) {
648 foreach ( $this->additional_meta as $key ) {
649 //phpcs:ignore WordPress.Security.NonceVerification.Missing
650 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
651 }
652 }
653
654 do_action( 'tutor_save_course_after', $post_ID, $post );
655 }
656
657 /**
658 * Save course topic
659 *
660 * @since 1.0.0
661 * @return void
662 */
663 public function tutor_save_topic() {
664 tutor_utils()->checking_nonce();
665
666 // Check required fields.
667 if ( empty( Input::post( 'topic_title' ) ) ) {
668 wp_send_json_error( array( 'message' => __( 'Topic title is required!', 'tutor' ) ) );
669 }
670
671 // Gather parameters.
672 $course_id = Input::post( 'topic_course_id', 0, Input::TYPE_INT );
673 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
674 $topic_title = Input::post( 'topic_title' );
675 $topic_summery = Input::post( 'topic_summery', '', Input::TYPE_KSES_POST );
676 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
677
678 // Validate if user can manage the topic.
679 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
680 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
681 }
682
683 // Create payload to create/update the topic.
684 $post_arr = array(
685 'post_type' => 'topics',
686 'post_title' => $topic_title,
687 'post_content' => $topic_summery,
688 'post_status' => 'publish',
689 'post_author' => get_current_user_id(),
690 'post_parent' => $course_id,
691 'menu_order' => $next_topic_order_id,
692 );
693 $topic_id ? $post_arr['ID'] = $topic_id : 0;
694 $current_topic_id = wp_insert_post( $post_arr );
695
696 ob_start();
697 include tutor()->path . 'views/metabox/course-contents.php';
698
699 wp_send_json_success(
700 array(
701 'topic_title' => $topic_title,
702 'course_contents' => ob_get_clean(),
703 )
704 );
705 }
706
707 /**
708 * Add columns to course row in default WP list table
709 *
710 * @since 1.0.0
711 *
712 * @param array $columns column list.
713 * @return mixed
714 */
715 public function add_column( $columns ) {
716 $date_col = $columns['date'];
717 unset( $columns['date'] );
718 $columns['lessons'] = __( 'Lessons', 'tutor' );
719 $columns['students'] = __( 'Students', 'tutor' );
720 $columns['price'] = __( 'Price', 'tutor' );
721 $columns['date'] = $date_col;
722
723 return $columns;
724 }
725
726 /**
727 * Add data to custom column
728 *
729 * @since 1.0.0
730 *
731 * @param string $column column name.
732 * @param integer $post_id post ID.
733 *
734 * @return void
735 */
736 public function custom_lesson_column( $column, $post_id ) {
737 if ( 'lessons' === $column ) {
738 echo esc_html( tutor_utils()->get_lesson_count_by_course( $post_id ) );
739 }
740
741 if ( 'students' === $column ) {
742 echo esc_html( tutor_utils()->count_enrolled_users_by_course( $post_id ) );
743 }
744
745 if ( 'price' === $column ) {
746 $price = tutor_utils()->get_course_price( $post_id );
747 if ( $price ) {
748 $monetize_by = tutils()->get_option( 'monetize_by' );
749 if ( function_exists( 'wc_price' ) && 'wc' === $monetize_by ) {
750 echo wp_kses(
751 '<span class="tutor-label-success">' . wc_price( $price ) . '</span>',
752 array(
753 'span' => array( 'class' => true ),
754 )
755 );
756 } else {
757 echo wp_kses(
758 '<span class="tutor-label-success">' . $price . '</span>',
759 array( 'span' => array( 'class' => true ) )
760 );
761 }
762 } else {
763 echo esc_html( apply_filters( 'tutor-loop-default-price', __( 'free', 'tutor' ) ) );
764 }
765 }
766 }
767
768
769 /**
770 * Delete a course topic
771 *
772 * @since 1.0.0
773 * @return void
774 */
775 public function tutor_delete_topic() {
776
777 tutor_utils()->checking_nonce();
778
779 global $wpdb;
780 $topic_id = Input::post( 'topic_id', '' );
781
782 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
783 wp_send_json_error( array( 'message' => 'Access Forbidden' ) );
784 }
785
786 // Assign course ID to orphan content IDs since the topic will be deleted.
787 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
788 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
789 foreach ( $content_ids as $content_id ) {
790 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
791 // Actually all kind of contents.
792 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibillity.
793 }
794
795 // Set contents under the topic orphan.
796 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
797
798 // Then delete the topic from database.
799 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
800 wp_delete_post( $topic_id );
801
802 wp_send_json_success();
803 }
804
805 /**
806 * Handle enroll now action
807 *
808 * @since 1.0.0
809 * @return void
810 */
811 public function enroll_now() {
812
813 // Checking if action comes from Enroll form.
814 // phpcs:ignore WordPress.Security.NonceVerification.Missing
815 if ( tutor_utils()->array_get( 'tutor_course_action', tutor_sanitize_data( $_POST ) ) !== '_tutor_course_enroll_now' || ! isset( $_POST['tutor_course_id'] ) ) {
816 return;
817 }
818
819 // Checking Nonce.
820 tutor_utils()->checking_nonce();
821
822 $user_id = get_current_user_id();
823 if ( ! $user_id ) {
824 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
825 }
826
827 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
828 $user_id = get_current_user_id();
829
830 /**
831 * TODO: need to check purchase information
832 */
833
834 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
835
836 /**
837 * If is is not purchasable, it's free, and enroll right now
838 * If purchasable, then process purchase.
839 *
840 * @since: v.1.0.0
841 */
842 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
843 // Process purchase.
844
845 } else {
846 // Free enroll.
847 tutor_utils()->do_enroll( $course_id );
848 }
849
850 $referer_url = wp_get_referer();
851 wp_safe_redirect( $referer_url . '?nocache=' . time() );
852 exit;
853 }
854
855 /**
856 * Mark complete completed
857 *
858 * @since 1.0.0
859 * @return void
860 */
861 public function mark_course_complete() {
862 $tutor_action = Input::post( 'tutor_action' );
863 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
864 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
865 return;
866 }
867
868 // Checking nonce.
869 tutor_utils()->checking_nonce();
870
871 $user_id = get_current_user_id();
872
873 // TODO: need to show view if not signed_in.
874 if ( ! $user_id ) {
875 die( esc_html__( 'Please Sign-In', 'tutor' ) );
876 }
877
878 CourseModel::mark_course_as_completed( $course_id, $user_id );
879
880 $permalink = get_the_permalink( $course_id );
881
882 // Set temporary identifier to show review pop up.
883 self::set_review_popup_data( $user_id, $course_id, $permalink );
884
885 wp_safe_redirect( $permalink );
886 exit;
887 }
888
889 /**
890 * Set data for review popup.
891 *
892 * @since 2.2.5
893 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
894 *
895 * @param int $user_id user id.
896 * @param int $course_id course id.
897 *
898 * @return void
899 */
900 public static function set_review_popup_data( $user_id, $course_id ) {
901 if ( get_tutor_option( 'enable_course_review' ) ) {
902 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
903 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
904 $meta_key = User::get_review_popup_meta( $course_id );
905 add_user_meta( $user_id, $meta_key, $course_id, true );
906 }
907 }
908 }
909
910 /**
911 * Popup review form on course details
912 *
913 * @since 1.0.0
914 * @return void
915 */
916 public function popup_review_form() {
917 if ( is_user_logged_in() ) {
918 $user_id = get_current_user_id();
919 $course_id = get_the_ID();
920 $meta_key = User::get_review_popup_meta( $course_id );
921 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
922
923 if ( is_single() && $course_id === $review_course_id ) {
924 include tutor()->path . 'views/modal/review.php';
925 }
926 }
927 }
928
929 /**
930 * Review popup data clear
931 *
932 * @since 2.4.0
933 *
934 * @return void
935 */
936 public function clear_review_popup_data() {
937 tutils()->checking_nonce();
938
939 if ( is_user_logged_in() ) {
940 $user_id = get_current_user_id();
941 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
942
943 if ( $course_id ) {
944 $meta_key = User::get_review_popup_meta( $course_id );
945 delete_user_meta( $user_id, $meta_key, $course_id );
946 }
947
948 wp_send_json_success();
949 }
950 }
951
952 /**
953 * Delete course delete from frontend dashboard
954 *
955 * @since 2.0.0
956 * @return void
957 */
958 public function tutor_delete_dashboard_course() {
959 tutor_utils()->checking_nonce();
960
961 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
962 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
963 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
964 }
965
966 /**
967 * Co-instructor can not delete a course
968 *
969 * @since 2.1.6
970 */
971 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
972 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
973 }
974
975 // Check if user is only an instructor.
976 if ( ! current_user_can( 'administrator' ) ) {
977 // Check if instructor can trash course.
978 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
979
980 if ( ! $can_trash_post ) {
981 wp_send_json_error( tutor_utils()->error_message() );
982 }
983 }
984
985 $trash_course = wp_update_post(
986 array(
987 'ID' => $course_id,
988 'post_status' => 'trash',
989 )
990 );
991
992 if ( $trash_course ) {
993 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
994 }
995 wp_send_json_success();
996 }
997
998 /**
999 * Main author change from gutenberg editor
1000 *
1001 * @since 2.0.0
1002 *
1003 * @param array $data data.
1004 * @param array $postarr post array.
1005 *
1006 * @return mixed
1007 */
1008 public function tutor_add_gutenberg_author( $data, $postarr ) {
1009 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
1010 $post_type = $postarr['post_type'];
1011 $courses_post_type = tutor()->course_post_type;
1012
1013 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
1014 return $data;
1015 }
1016
1017 /**
1018 * Only admin can change main author
1019 */
1020 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
1021 global $wpdb;
1022 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
1023 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
1024
1025 if ( $post_author > 0 ) {
1026 $data['post_author'] = $post_author;
1027 } else {
1028 $data['post_author'] = get_current_user_id();
1029 }
1030 }
1031
1032 return $data;
1033 }
1034
1035
1036 /**
1037 * Attach product with course when course save from frontend or backend.
1038 *
1039 * @since 1.3.4
1040 *
1041 * @param integer $post_ID course ID.
1042 * @param array $post_data cretaed course post details.
1043 *
1044 * @return void
1045 */
1046 public function attach_product_with_course( $post_ID, $post_data ) {
1047
1048 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1049
1050 /**
1051 * The function is_admin will check only loaded page from WP admin.
1052 * It does not check any role
1053 *
1054 * @since 2.6.0
1055 *
1056 * tutor_is_rest() check added, if loaded from rest api
1057 */
1058 $is_admin_panel = is_admin() || tutor_is_rest();
1059 // From backend course select box.
1060 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
1061
1062 /**
1063 * From Admin Panel, Free user can only select product from dropdown
1064 */
1065 if ( $is_admin_panel && 'wc' === $monetize_by && tutor()->has_pro === false ) {
1066 if ( $product_id > 0 ) {
1067 update_post_meta( $post_ID, '_tutor_course_product_id', $product_id );
1068 } elseif ( -1 === $product_id ) {
1069 if ( ! tutor_is_rest() ) {
1070 delete_post_meta( $post_ID, '_tutor_course_product_id' );
1071 }
1072 }
1073
1074 return;
1075 }
1076
1077 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
1078 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
1079 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
1080
1081 if ( ! $course_price || $sale_price >= $course_price ) {
1082 return;
1083 }
1084
1085 $course = get_post( $post_ID );
1086
1087 if ( 'wc' === $monetize_by ) {
1088
1089 $is_update = false;
1090 if ( $attached_product_id ) {
1091 $wc_product = get_post_meta( $attached_product_id, '_product_version', true );
1092 if ( $wc_product ) {
1093 $is_update = true;
1094 }
1095 }
1096
1097 if ( $is_update || ( $product_id > 0 && $is_admin_panel ) ) {
1098 // Added in @since 2.0.7.
1099 if ( $product_id > 0 && $is_admin_panel ) {
1100 $attached_product_id = $product_id;
1101 update_post_meta( $post_ID, '_tutor_course_product_id', $product_id );
1102 }
1103
1104 $product_obj = wc_get_product( $attached_product_id );
1105 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $attached_product_id );
1106 if ( $product_obj->is_type( 'subscription' ) ) {
1107 update_post_meta( $attached_product_id, '_subscription_price', $course_price );
1108 }
1109 } else {
1110 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
1111 if ( $product_id ) {
1112 update_post_meta( $post_ID, '_tutor_course_product_id', $product_id );
1113 // Mark product for woocommerce.
1114 update_post_meta( $product_id, '_virtual', 'yes' );
1115 update_post_meta( $product_id, '_tutor_product', 'yes' );
1116
1117 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
1118 if ( $course_post_thumbnail ) {
1119 set_post_thumbnail( $product_id, $course_post_thumbnail );
1120 }
1121 }
1122 }
1123 } elseif ( 'edd' === $monetize_by ) {
1124
1125 $is_update = false;
1126
1127 if ( $attached_product_id ) {
1128 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
1129 if ( $edd_price ) {
1130 $is_update = true;
1131 }
1132 }
1133
1134 if ( $is_update ) {
1135 // Update the product.
1136 update_post_meta( $attached_product_id, 'edd_price', $course_price );
1137 } else {
1138 // Create new product.
1139
1140 $post_arr = array(
1141 'post_type' => 'download',
1142 'post_title' => $course->post_title,
1143 'post_status' => 'publish',
1144 'post_author' => get_current_user_id(),
1145 );
1146 $download_id = wp_insert_post( $post_arr );
1147 if ( $download_id ) {
1148 // EDD edd_price.
1149 update_post_meta( $download_id, 'edd_price', $course_price );
1150
1151 update_post_meta( $post_ID, '_tutor_course_product_id', $download_id );
1152 // Mark product for EDD.
1153 update_post_meta( $download_id, '_tutor_product', 'yes' );
1154
1155 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
1156 if ( $course_post_thumbnail ) {
1157 set_post_thumbnail( $download_id, $course_post_thumbnail );
1158 }
1159 }
1160 }
1161 }
1162 }
1163
1164 /**
1165 * Add Course level to course settings
1166 *
1167 * @since 1.4.1
1168 *
1169 * @param array $args arguments.
1170 * @return array
1171 */
1172 public function add_course_level_to_settings( $args ) {
1173 $course_id = get_the_ID();
1174 $levels = tutor_utils()->course_levels();
1175 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
1176
1177 $args['general']['fields']['_tutor_course_level'] = array(
1178 'type' => 'select',
1179 'label' => __( 'Difficulty Level', 'tutor' ),
1180 'label_title' => __( 'Enable', 'tutor' ),
1181 'options' => $levels,
1182 'value' => $course_level ? $course_level : 'intermediate',
1183 'desc' => __( 'Course difficulty level', 'tutor' ),
1184 );
1185
1186 return $args;
1187 }
1188
1189 /**
1190 * Check if course starting
1191 *
1192 * @since 1.4.8
1193 * @return void
1194 */
1195 public function tutor_lesson_load_before() {
1196 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
1197 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
1198 if ( is_user_logged_in() ) {
1199 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
1200 if ( ! $completed_lessons && ! $is_course_started ) {
1201 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
1202 do_action( 'tutor/course/started', $course_id );
1203 }
1204 }
1205 }
1206
1207 /**
1208 * Add Course level to course settings
1209 *
1210 * @since 1.4.8
1211 * @return void
1212 */
1213 public function course_elements_enable_disable() {
1214 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
1215 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
1216 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
1217 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
1218 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
1219 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
1220 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
1221 }
1222
1223 /**
1224 * Enable disable course progress bar
1225 *
1226 * @since 1.4.8
1227 *
1228 * @param string $html HTML string.
1229 * @return string
1230 */
1231 public function enable_disable_course_progress_bar( $html ) {
1232 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
1233 if ( $disable_option ) {
1234 return '';
1235 }
1236 return $html;
1237 }
1238
1239 /**
1240 * Enable disable material includes
1241 *
1242 * @since 1.4.8
1243 *
1244 * @param string $html HTML string.
1245 * @return string
1246 */
1247 public function enable_disable_material_includes( $html ) {
1248 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
1249 if ( $disable_option ) {
1250 return '';
1251 }
1252 return $html;
1253 }
1254
1255 /**
1256 * Enable disable course content
1257 *
1258 * @since 1.4.8
1259 *
1260 * @param string $html HTML string.
1261 * @return string
1262 */
1263 public function enable_disable_course_content( $html ) {
1264 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
1265 if ( $disable_option ) {
1266 return '';
1267 }
1268 return $html;
1269 }
1270
1271 /**
1272 * Enable disable course benefits
1273 *
1274 * @since 1.4.8
1275 *
1276 * @param string $html HTML string.
1277 * @return string
1278 */
1279 public function enable_disable_course_benefits( $html ) {
1280 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
1281 if ( $disable_option ) {
1282 return '';
1283 }
1284 return $html;
1285 }
1286
1287 /**
1288 * Enable disable course requirements
1289 *
1290 * @since 1.4.8
1291 *
1292 * @param string $html HTML string.
1293 * @return string
1294 */
1295 public function enable_disable_course_requirements( $html ) {
1296 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
1297 if ( $disable_option ) {
1298 return '';
1299 }
1300 return $html;
1301 }
1302
1303 /**
1304 * Enable disable course target audience
1305 *
1306 * @since 1.4.8
1307 *
1308 * @param string $html HTML string.
1309 * @return string
1310 */
1311 public function enable_disable_course_target_audience( $html ) {
1312 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
1313 if ( $disable_option ) {
1314 return '';
1315 }
1316 return $html;
1317 }
1318
1319 /**
1320 * Enable disable course nav items
1321 *
1322 * @since 1.4.8
1323 *
1324 * @param array $items item list.
1325 * @param integer $course_id course ID.
1326 *
1327 * @return array
1328 */
1329 public function enable_disable_course_nav_items( $items, $course_id ) {
1330 global $wp_query, $post;
1331 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
1332 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
1333 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
1334
1335 // Whether Q&A enabled.
1336 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
1337 if ( tutor_utils()->array_get( 'questions', $items ) ) {
1338 unset( $items['questions'] );
1339 }
1340 }
1341
1342 // Whether announcment enabled.
1343 if ( $disable_course_announcements ) {
1344 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
1345 unset( $items['announcements'] );
1346 }
1347 }
1348
1349 // Hide review section if disabled.
1350 if ( ! get_tutor_option( 'enable_course_review' ) ) {
1351 unset( $items['reviews'] );
1352 }
1353
1354 // Whether enrollment require.
1355 $is_enrolled = tutor_utils()->is_enrolled();
1356
1357 return array_filter(
1358 $items,
1359 function ( $item ) use ( $is_enrolled ) {
1360 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
1361 return $is_enrolled;
1362 }
1363 return true;
1364 }
1365 );
1366 }
1367
1368 /**
1369 * Filter product in shop page
1370 *
1371 * @since 1.4.9
1372 * @return void|null
1373 */
1374 public function filter_product_in_shop_page() {
1375 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
1376 if ( ! $hide_course_from_shop_page ) {
1377 return;
1378 }
1379 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
1380 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
1381 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
1382 }
1383
1384
1385 /**
1386 * Tutor product meta query
1387 *
1388 * @since 1.4.9
1389 * @return array
1390 */
1391 public function tutor_product_meta_query() {
1392 $meta_query = array(
1393 'key' => '_tutor_product',
1394 'compare' => 'NOT EXISTS',
1395 );
1396 return $meta_query;
1397 }
1398
1399 /**
1400 * Filter product in woocommerce shop page
1401 *
1402 * @since 1.4.9
1403 *
1404 * @param \WP_Query $wp_query WP Query instance.
1405 * @return \WP_Query
1406 */
1407 public function filter_woocommerce_product_query( $wp_query ) {
1408 $product_ids = $this->get_connected_wc_product_ids();
1409 $wp_query->set( 'post__not_in', $product_ids );
1410 return $wp_query;
1411 }
1412
1413 /**
1414 * Get connected woocommerce product ids for course and course bundle
1415 *
1416 * @since 2.7.2
1417 *
1418 * @return array
1419 */
1420 public function get_connected_wc_product_ids() {
1421 global $wpdb;
1422
1423 $results = $wpdb->get_results(
1424 $wpdb->prepare(
1425 "SELECT DISTINCT pm.meta_value product_id
1426 FROM {$wpdb->posts} p
1427 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
1428 AND pm.meta_key = %s
1429 WHERE post_type IN( 'courses','course-bundle' )",
1430 '_tutor_course_product_id'
1431 )
1432 );
1433
1434 $ids = array();
1435 if ( is_array( $results ) && count( $results ) ) {
1436 $ids = array_column( $results, 'product_id' );
1437 }
1438
1439 return $ids;
1440 }
1441
1442 /**
1443 * Filter product in edd downloads shortcode page
1444 *
1445 * @since 1.4.9
1446 *
1447 * @param \WP_Query $query WP Query instance.
1448 * @return \WP_Query
1449 */
1450 public function filter_edd_downloads_query( $query ) {
1451 $query['meta_query'][] = $this->tutor_product_meta_query();
1452 return $query;
1453 }
1454
1455 /**
1456 * Filter product in edd downloads archive page
1457 *
1458 * @since 1.4.9
1459 *
1460 * @param \WP_Query $wp_query WP Query instance.
1461 * @return \WP_Query
1462 */
1463 public function filter_archive_meta_query( $wp_query ) {
1464 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
1465 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
1466 }
1467 return $wp_query;
1468 }
1469
1470 /**
1471 * Removed course price if already enrolled at single course
1472 *
1473 * @since 1.5.8
1474 *
1475 * @param string $html HTML string.
1476 * @return string
1477 */
1478 public function remove_price_if_enrolled( $html ) {
1479 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
1480
1481 if ( $should_removed ) {
1482 $course_id = get_the_ID();
1483 $enrolled = tutor_utils()->is_enrolled( $course_id );
1484 if ( $enrolled ) {
1485 $html = '';
1486 }
1487 }
1488 return $html;
1489 }
1490
1491 /**
1492 * Check if all lessons and quizzes done before mark course complete.
1493 *
1494 * @since 1.5.8
1495 *
1496 * @param string $html HTML string.
1497 * @return string
1498 */
1499 public function tutor_lms_hide_course_complete_btn( $html ) {
1500
1501 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
1502 if ( 'strict' !== $completion_mode ) {
1503 return $html;
1504 }
1505
1506 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
1507 $lesson_count = tutor_utils()->get_lesson_count_by_course();
1508
1509 if ( $completed_lesson < $lesson_count ) {
1510 return '<div class="tutor-alert tutor-warning tutor-mt-28">
1511 <div class="tutor-alert-text">
1512 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
1513 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
1514 </div>
1515 </div>';
1516 }
1517
1518 $quizzes = array();
1519 $assignments = array();
1520
1521 $course_contents = tutor_utils()->get_course_contents_by_id();
1522 if ( tutor_utils()->count( $course_contents ) ) {
1523 foreach ( $course_contents as $content ) {
1524 if ( 'tutor_quiz' === $content->post_type ) {
1525 $quizzes[] = $content;
1526 }
1527 if ( 'tutor_assignments' === $content->post_type ) {
1528 $assignments[] = $content;
1529 }
1530 }
1531 }
1532
1533 $required_assignment_pass = 0;
1534
1535 foreach ( $assignments as $row ) {
1536
1537 $submitted_assignment = tutor_utils()->is_assignment_submitted( $row->ID );
1538 $is_reviewed_by_instructor = null === $submitted_assignment
1539 ? false
1540 : get_comment_meta( $submitted_assignment->comment_ID, 'evaluate_time', true );
1541
1542 if ( $submitted_assignment && $is_reviewed_by_instructor ) {
1543 $pass_mark = tutor_utils()->get_assignment_option( $submitted_assignment->comment_post_ID, 'pass_mark' );
1544 $given_mark = get_comment_meta( $submitted_assignment->comment_ID, 'assignment_mark', true );
1545
1546 if ( $given_mark < $pass_mark ) {
1547 $required_assignment_pass++;
1548 }
1549 } else {
1550 $required_assignment_pass++;
1551 }
1552 }
1553
1554 $is_quiz_pass = true;
1555 $required_quiz_pass = 0;
1556
1557 if ( tutor_utils()->count( $quizzes ) ) {
1558 foreach ( $quizzes as $quiz ) {
1559
1560 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
1561 if ( $attempt ) {
1562 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
1563 $earned_percentage = $attempt->earned_marks > 0 ? ( number_format( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) ) : 0;
1564
1565 if ( $earned_percentage < $passing_grade ) {
1566 $required_quiz_pass++;
1567 $is_quiz_pass = false;
1568 }
1569 } else {
1570 $required_quiz_pass++;
1571 $is_quiz_pass = false;
1572 }
1573 }
1574 }
1575
1576 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
1577 $_msg = '';
1578 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
1579 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
1580
1581 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
1582 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
1583 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
1584 }
1585
1586 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
1587 //phpcs:ignore
1588 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
1589 }
1590
1591 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
1592 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
1593 $_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 );
1594 }
1595
1596 return '<div class="tutor-alert tutor-warning tutor-mt-28">
1597 <div class="tutor-alert-text">
1598 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
1599 <span>' . $_msg . '</span>
1600 </div>
1601 </div>';
1602 }
1603
1604 return $html;
1605 }
1606
1607 /**
1608 * Generate Gradebook
1609 *
1610 * @since 1.5.8
1611 *
1612 * @param string $html HTML string.
1613 * @return string
1614 */
1615 public function get_generate_greadbook( $html ) {
1616 if ( ! tutor_utils()->is_completed_course() ) {
1617 return '';
1618 }
1619 return $html;
1620 }
1621
1622 /**
1623 * Add social share content in header
1624 *
1625 * @since 1.6.3
1626 * @return void
1627 */
1628 public function social_share_content() {
1629 global $wp_query, $post;
1630 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
1631 <!--Facebook-->
1632 <meta property="og:type" content="website"/>
1633 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
1634 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
1635 <!--Twitter-->
1636 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
1637 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
1638 <!--Google+-->
1639 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
1640 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
1641 <?php
1642 }
1643 }
1644
1645 /**
1646 * Delete associated enrollment
1647 *
1648 * @since 1.8.2
1649 *
1650 * @param integer $post_id post ID.
1651 * @return void
1652 */
1653 public function delete_associated_enrollment( $post_id ) {
1654 global $wpdb;
1655
1656 $enroll_id = $wpdb->get_var(
1657 $wpdb->prepare(
1658 "SELECT
1659 post_id
1660 FROM
1661 {$wpdb->postmeta}
1662 WHERE
1663 meta_key='_tutor_enrolled_by_order_id'
1664 AND meta_value = %d
1665 ",
1666 $post_id
1667 )
1668 );
1669
1670 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
1671
1672 $course_id = get_post_field( 'post_parent', $enroll_id );
1673 $user_id = get_post_field( 'post_author', $enroll_id );
1674
1675 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
1676 }
1677 }
1678
1679 /**
1680 * Reset course progress.
1681 *
1682 * @since 1.5.8
1683 * @return void
1684 */
1685 public function tutor_reset_course_progress() {
1686 tutor_utils()->checking_nonce();
1687 $course_id = Input::post( 'course_id' );
1688
1689 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
1690 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
1691 return;
1692 }
1693
1694 tutor_utils()->delete_course_progress( $course_id );
1695 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
1696 }
1697
1698 /**
1699 * Do enroll if guest attempt to enroll and course is free
1700 *
1701 * @since 1.9.8
1702 *
1703 * @param integer $course_id course ID.
1704 * @param integer $user_id user ID.
1705
1706 * @return void
1707 */
1708 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
1709 $course_id = sanitize_text_field( $course_id );
1710 if ( $course_id ) {
1711 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
1712 if ( ! $is_purchasable ) {
1713 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
1714 do_action( 'guest_attempt_after_enrollment', $course_id );
1715 }
1716 }
1717 }
1718
1719 /**
1720 * Handle course enrollment
1721 *
1722 * @since 2.1.0
1723 * @return void
1724 */
1725 public function course_enrollment() {
1726 tutor_utils()->checking_nonce();
1727
1728 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1729 $user_id = get_current_user_id();
1730
1731 if ( $course_id ) {
1732 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
1733 if ( $enroll ) {
1734 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
1735 } else {
1736 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
1737 }
1738 } else {
1739 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
1740 }
1741 }
1742
1743 /**
1744 * After trash a course direct to the course list page
1745 *
1746 * @since 2.1.7
1747 *
1748 * @param integer $post_id int course id.
1749 *
1750 * @return void
1751 */
1752 public static function redirect_to_course_list_page( int $post_id ): void {
1753 $post = get_post( $post_id );
1754 if ( tutor()->course_post_type === $post->post_type ) {
1755 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
1756 if ( ! $is_gutenberg_enabled ) {
1757 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
1758 exit;
1759 }
1760 }
1761 }
1762
1763 /**
1764 * Create or update WooCommerce product
1765 *
1766 * If product id not set it will create new one.
1767 *
1768 * @since 2.2.0
1769 *
1770 * @param string $title product title.
1771 * @param string $reg_price product price.
1772 * @param string $sale_price product sale price.
1773 * @param int $product_id product ID.
1774 * @param string $status product status.
1775 *
1776 * @return integer
1777 */
1778 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
1779 $product_obj = new \WC_Product();
1780 if ( $product_id ) {
1781 $product_obj = wc_get_product( $product_id );
1782 }
1783
1784 $product_obj->set_name( $title );
1785 $product_obj->set_status( $status );
1786 $product_obj->set_price( $reg_price );
1787 $product_obj->set_regular_price( $reg_price );
1788
1789 if ( $sale_price > 0 ) {
1790 $product_obj->set_sale_price( $sale_price );
1791 } else {
1792 $product_obj->set_sale_price( null );
1793 }
1794
1795 $product_obj->set_sold_individually( true );
1796
1797 return $product_obj->save();
1798 }
1799 }
1800