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