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