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