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