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