PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.4.2
Tutor LMS – eLearning and online course solution v3.4.2
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / Course.php
tutor / classes Last commit date
Addons.php 1 year ago Admin.php 1 year ago Ajax.php 1 year ago Announcements.php 1 year ago Assets.php 1 year ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Course.php 1 year ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 1 year ago Course_Settings_Tabs.php 1 year ago Course_Widget.php 1 year ago Custom_Validation.php 3 years ago Dashboard.php 1 year ago Earnings.php 1 year ago FormHandler.php 2 years ago Frontend.php 1 year ago Gutenberg.php 1 year ago Input.php 1 year ago Instructor.php 1 year ago Instructors_List.php 1 year ago Lesson.php 1 year ago Options_V2.php 1 year ago Permalink.php 2 years ago Post_types.php 1 year ago Private_Course_Access.php 1 year ago Q_And_A.php 1 year ago Question_Answers_List.php 3 years ago Quiz.php 1 year ago QuizBuilder.php 1 year ago Quiz_Attempts_List.php 1 year ago RestAPI.php 2 years ago Reviews.php 3 years ago Rewrite_Rules.php 2 years ago Shortcode.php 1 year ago Singleton.php 1 year ago Student.php 1 year ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 1 year ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 1 year ago Tutor.php 1 year ago TutorEDD.php 1 year ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 year ago Upgrader.php 1 year ago User.php 1 year ago Utils.php 1 year ago Video_Stream.php 3 years ago WhatsNew.php 2 years ago Withdraw.php 1 year ago Withdraw_Requests_List.php 1 year ago WooCommerce.php 1 year ago
Course.php
3075 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 stdClass;
18 use TUTOR\Input;
19 use Tutor\Helpers\HttpHelper;
20 use Tutor\Models\CourseModel;
21 use Tutor\Ecommerce\Ecommerce;
22 use Tutor\Traits\JsonResponse;
23 use Tutor\Helpers\ValidationHelper;
24 use TutorPro\CourseBundle\Models\BundleModel;
25
26 /**
27 * Course Class
28 *
29 * @since 1.0.0
30 */
31 class Course extends Tutor_Base {
32 use JsonResponse;
33
34 /**
35 * Course Price type
36 *
37 * @since 3.0.0
38 *
39 * @var string
40 */
41 const PRICE_TYPE_FREE = 'free';
42 const PRICE_TYPE_PAID = 'paid';
43 const PRICE_TYPE_SUBSCRIPTION = 'subscription';
44
45 /**
46 * Course price and sale price
47 *
48 * @since 3.0.0
49 */
50 const COURSE_PRICE_TYPE_META = '_tutor_course_price_type';
51 const COURSE_PRICE_META = 'tutor_course_price';
52 const COURSE_SALE_PRICE_META = 'tutor_course_sale_price';
53 const COURSE_SELLING_OPTION_META = 'tutor_course_selling_option';
54 const COURSE_PRODUCT_ID_META = '_tutor_course_product_id';
55
56 /**
57 * Selling option constants
58 *
59 * @since 3.0.0
60 */
61 const SELLING_OPTION_ONE_TIME = 'one_time';
62 const SELLING_OPTION_SUBSCRIPTION = 'subscription';
63 const SELLING_OPTION_BOTH = 'both'; // onetime and subscription.
64 const SELLING_OPTION_MEMBERSHIP = 'membership';
65 const SELLING_OPTION_ALL = 'all';
66
67
68 /**
69 * Additional course meta info
70 *
71 * @var array
72 */
73 private $additional_meta = array(
74 '_tutor_enable_qa',
75 '_tutor_is_public_course',
76 );
77
78 /**
79 * Constructor
80 *
81 * @since 1.0.0
82 * @since 3.0.0 $register_hooks param added to reuse this class.
83 *
84 * @param bool $register_hooks register hooks.
85 *
86 * @return void
87 */
88 public function __construct( $register_hooks = true ) {
89 parent::__construct();
90
91 if ( ! $register_hooks ) {
92 return;
93 }
94
95 add_action( 'save_post_' . $this->course_post_type, array( $this, 'save_course_meta' ), 10, 2 );
96
97 add_action( 'wp_ajax_tutor_save_topic', array( $this, 'tutor_save_topic' ) );
98 add_action( 'wp_ajax_tutor_delete_topic', array( $this, 'tutor_delete_topic' ) );
99
100 /**
101 * Frontend Action
102 */
103 add_action( 'template_redirect', array( $this, 'enroll_now' ) );
104 add_action( 'init', array( $this, 'mark_course_complete' ) );
105
106 /**
107 * Frontend Dashboard
108 */
109 add_action( 'wp_ajax_tutor_delete_dashboard_course', array( $this, 'tutor_delete_dashboard_course' ) );
110
111 /**
112 * Gutenberg author support
113 */
114 add_filter( 'wp_insert_post_data', array( $this, 'tutor_add_gutenberg_author' ), 99, 2 );
115
116 /**
117 * Do Stuff for the course save from frontend
118 */
119 add_action( 'save_tutor_course', array( $this, 'attach_product_with_course' ), 10, 2 );
120
121 /**
122 * Add course level to course settings
123 *
124 * @since v.1.4.1
125 */
126 add_filter( 'tutor_course_settings_tabs', array( $this, 'add_course_level_to_settings' ) );
127
128 /**
129 * Enable Disable Course Details Page Feature
130 *
131 * @since v.1.4.8
132 */
133 $this->course_elements_enable_disable();
134
135 /**
136 * Check if course starting, set meta if starting
137 *
138 * @since v.1.4.8
139 */
140 add_action( 'tutor_lesson_load_before', array( $this, 'tutor_lesson_load_before' ) );
141
142 /**
143 * Filter product in shop page
144 *
145 * @since v.1.4.9
146 */
147 $this->filter_product_in_shop_page();
148
149 /**
150 * Remove the course price if enrolled
151 *
152 * @since 1.5.8
153 */
154 add_filter( 'tutor_course_price', array( $this, 'remove_price_if_enrolled' ) );
155
156 /**
157 * Remove course complete button if course completion is strict mode
158 *
159 * @since v.1.6.1
160 */
161 add_filter( 'tutor_course/single/complete_form', array( $this, 'tutor_lms_hide_course_complete_btn' ) );
162 add_filter( 'get_gradebook_generate_form_html', array( $this, 'get_generate_greadbook' ) );
163
164 /**
165 * Add social share content in header
166 *
167 * @since v.1.6.3
168 */
169 add_action( 'wp_head', array( $this, 'social_share_content' ) );
170
171 /**
172 * Delete course data after deleted course
173 *
174 * @since v.1.6.6
175 */
176 add_action( 'deleted_post', array( new CourseModel(), 'delete_course_data' ) );
177
178 /**
179 * Delete course data after deleted course
180 *
181 * @since v.1.8.2
182 */
183 add_action( 'before_delete_post', array( $this, 'delete_associated_enrollment' ) );
184
185 /**
186 * Show only own uploads in media library if user is instructor
187 *
188 * @since v1.8.9
189 */
190 add_filter( 'posts_where', array( $this, 'restrict_media' ) );
191
192 /**
193 * Restrict new enrol/purchase button if course member limit reached
194 *
195 * @since v1.9.0
196 */
197 add_filter( 'tutor_course_restrict_new_entry', array( $this, 'restrict_new_student_entry' ) );
198
199 /**
200 * Reset course progress on retake
201 *
202 * @since v1.9.5
203 */
204 add_action( 'wp_ajax_tutor_reset_course_progress', array( $this, 'tutor_reset_course_progress' ) );
205
206 /**
207 * Popup for review
208 *
209 * @since v1.9.7
210 */
211 add_action( 'wp_footer', array( $this, 'popup_review_form' ) );
212 add_action( 'wp_ajax_tutor_clear_review_popup_data', array( $this, 'clear_review_popup_data' ) );
213
214 /**
215 * Do enroll after login if guest take enroll attempt
216 *
217 * @since 1.9.8
218 */
219 add_action( 'tutor_do_enroll_after_login_if_attempt', array( $this, 'enroll_after_login_if_attempt' ), 10, 2 );
220
221 add_action( 'wp_ajax_tutor_update_course_content_order', array( $this, 'tutor_update_course_content_order' ) );
222
223 add_action( 'wp_ajax_tutor_get_wc_product', array( $this, 'get_wc_product' ) );
224 add_action( 'wp_ajax_tutor_get_wc_products', array( $this, 'get_wc_products' ) );
225
226 add_action( 'wp_ajax_tutor_course_enrollment', array( $this, 'course_enrollment' ) );
227
228 /**
229 * After trash a course redirect to course list page
230 *
231 * @since 2.1.7
232 */
233 add_action( 'trashed_post', __CLASS__ . '::redirect_to_course_list_page' );
234
235 add_filter( 'tutor_enroll_required_login_class', array( $this, 'add_enroll_required_login_class' ) );
236
237 /**
238 * Remove wp trash button if instructor settings is disabled
239 *
240 * @since 2.7.3
241 */
242 add_action( 'tutor_option_save_after', array( $this, 'disable_course_trash_instructor' ) );
243
244 /**
245 * New course builder
246 *
247 * @since 3.0.0
248 */
249 add_action( 'template_redirect', array( $this, 'load_course_builder' ) );
250 add_action( 'tutor_before_course_builder_load', array( $this, 'enqueue_course_builder_assets' ) );
251 add_filter( 'tutor_localize_data', array( $this, 'localize_course_builder_data' ) );
252
253 /**
254 * Ajax list
255 *
256 * @since 3.0.0
257 */
258 add_action( 'wp_ajax_tutor_create_new_draft_course', array( $this, 'ajax_create_new_draft_course' ) );
259 add_action( 'wp_ajax_tutor_course_list', array( $this, 'ajax_course_list' ) );
260 add_action( 'wp_ajax_tutor_create_course', array( $this, 'ajax_create_course' ) );
261 add_action( 'wp_ajax_tutor_course_details', array( $this, 'ajax_course_details' ) );
262 add_action( 'wp_ajax_tutor_course_contents', array( $this, 'ajax_course_contents' ) );
263 add_action( 'wp_ajax_tutor_update_course', array( $this, 'ajax_update_course' ) );
264 add_action( 'wp_ajax_tutor_unlink_page_builder', array( $this, 'ajax_unlink_page_builder' ) );
265
266 add_filter( 'tutor_user_list_access', array( $this, 'user_list_access_for_instructor' ) );
267 add_filter( 'tutor_user_list_args', array( $this, 'user_list_args_for_instructor' ) );
268
269 add_filter( 'template_include', array( $this, 'handle_password_protected' ) );
270 add_action( 'login_form_postpass', array( $this, 'handle_password_submit' ) );
271 }
272
273 /**
274 * Handle password protected course and bundle form submission.
275 *
276 * @since 3.2.1
277 *
278 * @return void
279 */
280 public function handle_password_submit() {
281 if ( Input::has( 'post_password' ) && Input::has( 'course_id' ) ) {
282 $course_id = Input::post( 'course_id', 0, Input::TYPE_NUMERIC );
283 $password_required = post_password_required( $course_id );
284 if ( $password_required ) {
285 set_transient( 'tutor_post_password_error', __( 'Invalid password', 'tutor' ) );
286 }
287 }
288 }
289
290 /**
291 * Handle password protected course/bundle.
292 *
293 * @since 3.0.0
294 *
295 * @param string $template template path.
296 *
297 * @return string template path.
298 */
299 public function handle_password_protected( $template ) {
300 if ( is_single() ) {
301 $current_post_type = get_post_type( get_the_ID() );
302 $post_types = array( tutor()->course_post_type, tutor()->bundle_post_type );
303 if ( in_array( $current_post_type, $post_types, true ) && post_password_required() ) {
304 remove_all_filters( 'template_include' );
305 return tutor()->path . '/templates/single/password-protected.php';
306 }
307 }
308
309 return $template;
310 }
311
312 /**
313 * Remove move to trash button on WordPress editor for instructor.
314 *
315 * @since 2.7.3
316 *
317 * @return void
318 */
319 public function disable_course_trash_instructor() {
320 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
321 $role = get_role( tutor()->instructor_role );
322 if ( ! $can_trash_post ) {
323 $role->remove_cap( 'delete_tutor_courses' );
324 $role->remove_cap( 'delete_tutor_course' );
325 } else {
326 $role->add_cap( 'delete_tutor_courses' );
327 $role->add_cap( 'delete_tutor_course' );
328 }
329 }
330
331 /**
332 * Check if the video source type is valid
333 *
334 * @since 3.0.0
335 *
336 * @param string $source_type source type.
337 *
338 * @return boolean
339 */
340 private function is_valid_video_source_type( string $source_type ): bool {
341 $supported_types = tutor_utils()->get_option( 'supported_video_sources', array() );
342 if ( is_string( $supported_types ) ) {
343 $supported_types = array( $supported_types );
344 }
345
346 return in_array( $source_type, $supported_types, true );
347 }
348
349 /**
350 * Get course selling options.
351 *
352 * @since 3.0.0
353 *
354 * @return array
355 */
356 public static function get_selling_options() {
357 return array(
358 self::SELLING_OPTION_ONE_TIME,
359 self::SELLING_OPTION_SUBSCRIPTION,
360 self::SELLING_OPTION_BOTH,
361 self::SELLING_OPTION_MEMBERSHIP,
362 self::SELLING_OPTION_ALL,
363 );
364 }
365
366 /**
367 * Get course selling option
368 *
369 * @since 3.0.0
370 *
371 * @param int $course_id course id.
372 *
373 * @return string
374 */
375 public static function get_selling_option( $course_id ) {
376 return get_post_meta( $course_id, self::COURSE_SELLING_OPTION_META, true );
377 }
378
379 /**
380 * Validate video source
381 *
382 * @since 3.0.0
383 *
384 * @param array $params array of params.
385 * @param array $errors array of errors.
386 *
387 * @return void
388 */
389 public function validate_video_source( $params, &$errors ) {
390 if ( isset( $params['video'] ) ) {
391 $video_source_type = isset( $params['video']['source'] ) ? $params['video']['source'] : '';
392 if ( tutor_is_rest() ) {
393 $video_source_type = isset( $params['video']['source_type'] ) ? $params['video']['source_type'] : '';
394 }
395
396 if ( '' === $video_source_type ) {
397 $errors['video_source'] = __( 'Video source is required', 'tutor' );
398 } elseif ( ! $this->is_valid_video_source_type( $video_source_type ) ) {
399 $errors['video_source'] = __( 'Invalid video source', 'tutor' );
400 }
401 }
402 }
403
404 /**
405 * Validate scheduled courses
406 *
407 * @since 3.3.0
408 *
409 * @param array $params array of params.
410 * @param array $errors array of errors.
411 *
412 * @return void
413 */
414 public function validate_scheduled_course( $params, &$errors ) {
415 if ( isset( $params['post_status'] ) && isset( $params['course_settings'] ) ) {
416 if ( 'future' !== $params['post_status'] ) {
417 return;
418 }
419
420 $course_settings = $params['course_settings'];
421
422 if ( $course_settings['course_enrollment_period'] && 'no' === $course_settings['course_enrollment_period'] ) {
423 return;
424 }
425
426 if ( isset( $course_settings['enrollment_starts_at'] ) && ! empty( $course_settings['enrollment_starts_at'] ) ) {
427 $enrollment_start = strtotime( $course_settings['enrollment_starts_at'] );
428 $scheduled_date = strtotime( $params['post_date_gmt'] );
429
430 if ( $enrollment_start < $scheduled_date ) {
431 $errors['scheduled_course'] = __( 'The enrollment start date cannot be earlier than the course start date', 'tutor' );
432 }
433 }
434 }
435 }
436
437 /**
438 * Prepare course categories & tags
439 *
440 * @since 3.0.0
441 *
442 * @param array $params post params.
443 * @param array $errors array of errors.
444 *
445 * @return void
446 */
447 public function prepare_course_cats_tags( &$params, &$errors ) {
448 if ( isset( $params['course_categories'] ) ) {
449 if ( ! is_array( $params['course_categories'] ) || empty( $params['course_categories'] ) ) {
450 $errors['course_categories'] = __( 'Invalid course categories', 'tutor' );
451 } else {
452 $params['course_categories'] = $params['course_categories'];
453 }
454 }
455
456 if ( isset( $params['course_tags'] ) ) {
457 if ( ! is_array( $params['course_tags'] ) || empty( $params['course_tags'] ) ) {
458 $errors['course_tags'] = __( 'Invalid course tags', 'tutor' );
459 } else {
460 $params['course_tags'] = $params['course_tags'];
461 }
462 }
463 }
464
465 /**
466 * Setup course categories and tags
467 *
468 * @since 3.0.0
469 *
470 * @param int $post_id post id.
471 * @param array $params array of params.
472 *
473 * @return void
474 */
475 public function setup_course_categories_tags( $post_id, $params ) {
476 if ( isset( $params['course_categories'] ) && is_array( $params['course_categories'] ) ) {
477 $category_names = array();
478
479 foreach ( $params['course_categories'] as $category_id ) {
480 $term = get_term( $category_id, CourseModel::COURSE_CATEGORY );
481
482 if ( ! is_wp_error( $term ) && $term ) {
483 $category_names[] = $term->name;
484 }
485 }
486
487 // Set category names on the post.
488 wp_set_object_terms( $post_id, $category_names, CourseModel::COURSE_CATEGORY );
489 } else {
490 wp_set_object_terms( $post_id, array(), CourseModel::COURSE_CATEGORY );
491 }
492
493 if ( isset( $params['course_tags'] ) && is_array( $params['course_tags'] ) ) {
494 $tag_names = array();
495
496 foreach ( $params['course_tags'] as $tag_id ) {
497 $term = get_term( $tag_id, CourseModel::COURSE_TAG );
498
499 if ( ! is_wp_error( $term ) && $term ) {
500 $tag_names[] = $term->name;
501 }
502 }
503
504 // Set tag names on the post.
505 wp_set_object_terms( $post_id, $tag_names, CourseModel::COURSE_TAG );
506 } else {
507 wp_set_object_terms( $post_id, array(), CourseModel::COURSE_TAG );
508 }
509 }
510
511 /**
512 * Validate price for course create
513 *
514 * @since 3.0.0
515 *
516 * @param array $params array of params.
517 * @param array $errors array of errors.
518 *
519 * @return void
520 */
521 public function validate_price( $params, &$errors ) {
522 if ( isset( $params['pricing'] ) ) {
523 $type = $params['pricing']['type'] ?? '';
524
525 if ( '' === $type || ! in_array( $type, array( self::PRICE_TYPE_FREE, self::PRICE_TYPE_PAID ), true ) ) {
526 $errors['pricing'] = __( 'Invalid price type', 'tutor' );
527 }
528
529 if ( self::PRICE_TYPE_PAID === $type ) {
530 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
531 if ( 'wc' === $monetize_by ) {
532 $product_id = (int) isset( $params['pricing']['product_id'] ) ? $params['pricing']['product_id'] : 0;
533 // $product_id = 0 then new WC product will be created.
534 if ( $product_id ) {
535 $product = wc_get_product( $product_id );
536 if ( is_a( $product, 'WC_Product' ) ) {
537 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
538 if ( $is_linked_with_course ) {
539 $errors['pricing'] = __( 'Product already linked with course', 'tutor' );
540 }
541 }
542 }
543 }
544 }
545 }
546 }
547
548 /**
549 * Validate price
550 *
551 * @since 3.0.0
552 *
553 * @param array $params array of params.
554 * @param array $errors array of errors.
555 * @param int $course_id course id.
556 *
557 * @return void
558 */
559 public function validate_price_for_update( $params, &$errors, $course_id ) {
560 if ( isset( $params['pricing'] ) ) {
561 $type = $params['pricing']['type'] ?? '';
562
563 if ( '' === $type || ! in_array( $type, array( self::PRICE_TYPE_FREE, self::PRICE_TYPE_PAID, self::PRICE_TYPE_SUBSCRIPTION ), true ) ) {
564 $errors['pricing'] = __( 'Invalid price type', 'tutor' );
565 }
566
567 if ( self::PRICE_TYPE_PAID === $type ) {
568 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
569 if ( 'wc' === $monetize_by ) {
570 $course_product_id = tutor_utils()->get_course_product_id( $course_id );
571 $product_id = isset( $params['pricing']['product_id'] ) ? (int) $params['pricing']['product_id'] : 0;
572
573 if ( $product_id ) {
574 $product = wc_get_product( $product_id );
575 if ( is_a( $product, 'WC_Product' ) ) {
576 if ( $course_product_id !== $product_id ) {
577 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
578 if ( $is_linked_with_course ) {
579 $errors['pricing'] = __( 'Product already linked with course', 'tutor' );
580 }
581 }
582 } else {
583 $errors['pricing'] = __( 'Invalid product', 'tutor' );
584 }
585 } else {
586 /**
587 * If user does not select WC product
588 * Then automatic WC product will be create name with course title.
589 */
590 if ( ! isset( $params['course_price'] ) || ! floatval( $params['course_price'] ) ) {
591 $errors['pricing'] = __( 'Price is required', 'tutor' );
592 }
593 }
594 }
595 }
596 }
597 }
598
599 /**
600 * Prepare course meta data for update
601 *
602 * @param array $params params.
603 *
604 * @return void
605 */
606 public function prepare_create_post_meta( $params ) {
607 $additional_content = isset( $params['additional_content'] ) ? $params['additional_content'] : array();
608
609 $course_benefits = isset( $additional_content['course_benefits'] ) ? $additional_content['course_benefits'] : '';
610
611 $course_target_audience = isset( $additional_content['course_target_audience'] ) ? $additional_content['course_target_audience'] : '';
612
613 $course_duration = isset( $additional_content['course_duration'] ) ? array(
614 'hours' => $additional_content['course_duration']['hours'] ?? '',
615 'minutes' => $additional_content['course_duration']['minutes'] ?? '',
616 ) : array();
617
618 $course_materials = isset( $additional_content['course_material_includes'] ) ? $additional_content['course_material_includes'] : '';
619
620 $course_requirements = isset( $additional_content['course_requirements'] ) ? $additional_content['course_requirements'] : '';
621
622 $pricing = isset( $params['pricing'] ) ? array(
623 'type' => $params['pricing']['type'] ?? self::PRICE_TYPE_FREE,
624 'product_id' => (int) $params['pricing']['product_id'] ?? -1,
625 ) : array(
626 'type' => self::PRICE_TYPE_FREE,
627 'product_id' => -1,
628 );
629
630 // Setup global $_POST array.
631 $_POST['_tutor_course_additional_data_edit'] = true;
632
633 $_POST['tutor_course_price_type'] = $pricing['type'];
634 $_POST['course_duration'] = $course_duration;
635 $_POST['tutor_course_price_type'] = $pricing['type'];
636 $_POST['_tutor_course_product_id'] = $pricing['product_id'];
637 $_POST['_tutor_course_level'] = $params['course_level'];
638 $_POST['course_benefits'] = $course_benefits;
639 $_POST['course_requirements'] = $course_requirements;
640 $_POST['course_target_audience'] = $course_target_audience;
641 $_POST['course_material_includes'] = $course_materials;
642
643 if ( isset( $params['enable_qna'] ) && 'yes' === $params['enable_qna'] ) {
644 $_POST['_tutor_enable_qa'] = 'yes';
645 }
646
647 if ( isset( $params['_tutor_is_public_course'] ) && 'yes' === $params['_tutor_is_public_course'] ) {
648 $_POST['_tutor_is_public_course'] = 'yes';
649 }
650
651 // Set course price.
652 if ( -1 !== $pricing['product_id'] ) {
653 $product = wc_get_product( $pricing['product_id'] );
654 if ( is_a( $product, 'WC_Product' ) ) {
655 $regular_price = $product->get_regular_price();
656 $sale_price = $product->get_sale_price();
657
658 $_POST['course_price'] = $regular_price;
659 $_POST['course_sale_price'] = $sale_price;
660 }
661 }
662 }
663
664 /**
665 * Prepare course meta data for update
666 *
667 * @since 3.0.0
668 *
669 * @param array $params params.
670 *
671 * @throws \Exception Throw new exception.
672 *
673 * @return mixed
674 */
675 public function prepare_update_post_meta( $params ) {
676 $post_id = (int) $params['ID'];
677
678 $additional_content = isset( $params['additional_content'] ) ? $params['additional_content'] : array();
679
680 if ( ! empty( $additional_content ) ) {
681
682 $course_benefits = isset( $additional_content['course_benefits'] ) ? $additional_content['course_benefits'] : '';
683
684 $course_target_audience = isset( $additional_content['course_target_audience'] ) ? $additional_content['course_target_audience'] : '';
685
686 $course_duration = isset( $additional_content['course_duration'] ) ? array(
687 'hours' => $additional_content['course_duration']['hours'] ?? '',
688 'minutes' => $additional_content['course_duration']['minutes'] ?? '',
689 ) : array();
690
691 $course_materials = isset( $additional_content['course_material_includes'] ) ? $additional_content['course_material_includes'] : '';
692
693 $course_requirements = isset( $additional_content['course_requirements'] ) ? $additional_content['course_requirements'] : '';
694
695 if ( '' !== $course_benefits ) {
696 update_post_meta( $post_id, '_tutor_course_benefits', $course_benefits );
697 }
698
699 if ( '' !== $course_requirements ) {
700 update_post_meta( $post_id, '_tutor_course_requirements', $course_requirements );
701 }
702
703 if ( '' !== $course_target_audience ) {
704 update_post_meta( $post_id, '_tutor_course_target_audience', $course_target_audience );
705 }
706
707 if ( '' !== $course_materials ) {
708 update_post_meta( $post_id, '_tutor_course_material_includes', $course_materials );
709 }
710
711 if ( ! empty( $course_duration ) ) {
712 update_post_meta( $post_id, '_course_duration', $course_duration );
713 }
714 }
715
716 if ( isset( $params['pricing'] ) && ! empty( $params['pricing'] ) ) {
717 try {
718 if ( isset( $params['pricing']['type'] ) ) {
719 update_post_meta( $post_id, self::COURSE_PRICE_TYPE_META, $params['pricing']['type'] );
720 }
721 if ( isset( $params['pricing']['product_id'] ) ) {
722 update_post_meta( $post_id, '_tutor_course_product_id', $params['pricing']['product_id'] );
723 }
724 } catch ( \Throwable $th ) {
725 throw new \Exception( $th->getMessage() );
726 }
727 }
728
729 update_post_meta( $post_id, '_tutor_enable_qa', $params['enable_qna'] ?? 'yes' );
730 update_post_meta( $post_id, '_tutor_is_public_course', $params['is_public_course'] ?? 'no' );
731 update_post_meta( $post_id, '_tutor_course_level', $params['course_level'] );
732
733 do_action( 'tutor_after_prepare_update_post_meta', $post_id, $params );
734 }
735
736 /**
737 * Prepare course settings meta
738 *
739 * @since 3.0.0
740 *
741 * @param array $params params.
742 *
743 * @return void
744 */
745 public function prepare_course_settings( $params ) {
746 if ( isset( $params['course_settings'] ) ) {
747 $_POST['_tutor_course_settings'] = $params['course_settings'];
748 }
749 }
750
751 /**
752 * Check access before course builder ajax request.
753 *
754 * @since 3.0.0
755 *
756 * @param int $course_id course id.
757 *
758 * @return void
759 */
760 public function check_access( $course_id = null ) {
761 $has_access = false;
762
763 if ( $course_id ) {
764 $has_access = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
765 } else {
766 $has_access = User::is_admin() || User::is_instructor();
767 }
768
769 if ( ! $has_access ) {
770 $this->json_response(
771 tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ),
772 null,
773 HttpHelper::STATUS_UNAUTHORIZED
774 );
775 }
776 }
777
778 /**
779 * Validate request inputs.
780 *
781 * @param array $params input params.
782 * @param array $exclude exclude key from rules.
783 *
784 * @return object
785 */
786 public function validate_inputs( $params, $exclude = array() ) {
787 $status_str = implode( ',', CourseModel::get_status_list() );
788 $rules = array(
789 'course_id' => 'required|numeric',
790 'post_title' => 'required',
791 'post_author' => 'user_exists',
792 'post_status' => "required|match_string:{$status_str}",
793 'enable_qna' => 'if_input|match_string:yes,no',
794 'is_public_course' => 'if_input|match_string:yes,no',
795 );
796
797 foreach ( $exclude as $key ) {
798 if ( isset( $rules[ $key ] ) ) {
799 unset( $rules[ $key ] );
800 }
801 }
802
803 return ValidationHelper::validate( $rules, $params );
804 }
805
806 /**
807 * Create new draft course
808 *
809 * @since 3.0.0
810 *
811 * @return void JSON response
812 */
813 public function ajax_create_new_draft_course() {
814 tutor_utils()->check_nonce();
815
816 $this->check_access();
817
818 $course_id = wp_insert_post(
819 array(
820 'post_title' => __( 'New Course', 'tutor' ),
821 'post_type' => tutor()->course_post_type,
822 'post_status' => 'draft',
823 'post_name' => 'new-course',
824 )
825 );
826
827 if ( is_wp_error( $course_id ) ) {
828 $this->json_response( $course_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
829 }
830
831 update_post_meta( $course_id, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_FREE );
832
833 $link = admin_url( 'admin.php?page=create-course' );
834 if ( Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
835 $link = tutor_utils()->tutor_dashboard_url( 'create-course' );
836 }
837
838 $link = add_query_arg( array( 'course_id' => $course_id ), $link );
839
840 do_action( 'tutor_draft_course_created', $course_id );
841
842 $this->json_response(
843 __( 'Draft course created', 'tutor' ),
844 $link,
845 HttpHelper::STATUS_CREATED
846 );
847 }
848
849 /**
850 * Get course list
851 *
852 * @since 3.0.0
853 *
854 * @since 3.2.0
855 *
856 * Refactor the arguments & response as per new design
857 *
858 * @return void
859 */
860 public function ajax_course_list() {
861 $this->check_access();
862
863 $limit = Input::post( 'limit', 10, Input::TYPE_INT );
864 $offset = Input::post( 'offset', 0, Input::TYPE_INT );
865 $search_term = '';
866
867 $filter = json_decode( wp_unslash( $_POST['filter'] ) ); //phpcs:ignore --sanitized already
868 if ( ! empty( $filter ) && property_exists( $filter, 'search' ) ) {
869 $search_term = Input::sanitize( $filter->search );
870 }
871
872 $args = array(
873 'posts_per_page' => $limit,
874 'offset' => $offset,
875 's' => $search_term,
876 );
877
878 $exclude = Input::post( 'exclude', array(), Input::TYPE_ARRAY );
879 if ( count( $exclude ) ) {
880 $exclude = array_filter(
881 $exclude,
882 function ( $id ) {
883 return is_numeric( $id );
884 }
885 );
886 $args['post__not_in'] = $exclude;
887 }
888
889 $courses = CourseModel::get_courses_by_args( $args );
890
891 $response = array(
892 'results' => array(),
893 'total_items' => 0,
894 );
895
896 $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
897
898 if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
899 $courses = $courses->get_posts();
900 foreach ( $courses as $course ) {
901 $response['results'][] = self::get_mini_info( $course );
902 }
903 }
904
905 $this->json_response(
906 __( 'Course list retrieved successfully!', 'tutor' ),
907 $response
908 );
909 }
910
911 /**
912 * Create course by ajax request.
913 *
914 * @since 3.0.0
915 *
916 * @return void
917 */
918 public function ajax_create_course() {
919 tutor_utils()->check_nonce();
920
921 $this->check_access();
922
923 $params = Input::sanitize_array(
924 //phpcs:ignore WordPress.Security.NonceVerification.Missing
925 $_POST,
926 array(
927 'post_content' => 'wp_kses_post',
928 'course_material_includes' => 'sanitize_textarea_field',
929 )
930 );
931
932 $params['post_type'] = tutor()->course_post_type;
933
934 // Validate inputs.
935 $errors = array();
936 $validation = $this->validate_inputs( $params, array( 'course_id' ) );
937 if ( ! $validation->success ) {
938 $errors = $validation->errors;
939 }
940
941 if ( User::is_instructor() ) {
942 $params['post_author'] = get_current_user_id();
943 }
944
945 // Validate video source if user set video.
946 $this->validate_video_source( $params, $errors );
947
948 // Validate WC product.
949 $this->validate_price( $params, $errors );
950
951 // Set course categories and tags.
952 $this->prepare_course_cats_tags( $params, $errors );
953 $this->setup_course_price( $params );
954
955 if ( ! empty( $errors ) ) {
956 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
957 }
958
959 $this->prepare_course_settings( $params );
960
961 try {
962 $this->prepare_create_post_meta( $params );
963 } catch ( \Exception $e ) {
964 $this->json_response( $e->getMessage(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
965 }
966
967 $course_id = wp_insert_post( $params );
968 if ( is_wp_error( $course_id ) ) {
969 $this->json_response( $course_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
970 }
971
972 // Set course cats & tags.
973 $this->setup_course_categories_tags( $course_id, $params );
974
975 // Update course thumb.
976 if ( isset( $params['thumbnail_id'] ) ) {
977 set_post_thumbnail( $course_id, $params['thumbnail_id'] );
978 }
979
980 $this->json_response(
981 __( 'Course created successfully', 'tutor' ),
982 $course_id,
983 HttpHelper::STATUS_CREATED
984 );
985 }
986
987 /**
988 * Setup course price
989 *
990 * @since 3.0.0
991 *
992 * @param array $params params.
993 *
994 * @return void
995 */
996 public function setup_course_price( $params ) {
997 if ( isset( $params['pricing'] )
998 && isset( $params['pricing']['product_id'] )
999 && is_numeric( $params['pricing']['product_id'] ) ) {
1000 $_POST['_tutor_course_product_id'] = $params['pricing']['product_id'];
1001 }
1002 }
1003
1004 /**
1005 * Update course by ajax request.
1006 *
1007 * @since 3.0.0
1008 *
1009 * @return void
1010 */
1011 public function ajax_update_course() {
1012 tutor_utils()->check_nonce();
1013
1014 $params = Input::sanitize_array(
1015 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1016 wp_slash( $_POST ),
1017 array(
1018 'post_content' => 'wp_kses_post',
1019 'course_benefits' => 'sanitize_textarea_field',
1020 'course_target_audience' => 'sanitize_textarea_field',
1021 'course_material_includes' => 'sanitize_textarea_field',
1022 'course_requirements' => 'sanitize_textarea_field',
1023 )
1024 );
1025
1026 $course_id = (int) $params['course_id'];
1027 $this->check_access( $course_id );
1028
1029 $errors = array();
1030 $validation = $this->validate_inputs( $params );
1031 if ( ! $validation->success ) {
1032 $errors = $validation->errors;
1033 }
1034
1035 // Validate video source if user set video.
1036 $this->validate_video_source( $params, $errors );
1037
1038 // Validate WC product.
1039 $this->validate_price_for_update( $params, $errors, $course_id );
1040
1041 // Set course categories and tags.
1042 $this->prepare_course_cats_tags( $params, $errors );
1043
1044 $this->prepare_course_settings( $params );
1045
1046 // Validate scheduled courses.
1047 $this->validate_scheduled_course( $params, $errors );
1048
1049 $this->setup_course_price( $params );
1050
1051 if ( ! empty( $errors ) ) {
1052 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1053 }
1054
1055 /**
1056 * Can trash a course when user is admin or option `instructor_can_delete_course` is turned on.
1057 */
1058 if ( CourseModel::STATUS_TRASH === $params['post_status'] ) {
1059 if ( User::is_admin() || tutor_utils()->get_option( 'instructor_can_delete_course', false ) ) {
1060 $params['post_status'] = CourseModel::STATUS_TRASH;
1061 } else {
1062 unset( $params['post_status'] );
1063 }
1064 }
1065
1066 /**
1067 * Can publish a course when user is admin or option `instructor_can_publish_course` is turned on.
1068 * If instructor_can_publish_course is turned off then course status will be pending.
1069 */
1070 if ( CourseModel::STATUS_PUBLISH === $params['post_status'] ) {
1071 $is_instructor_allowed_to_publish = (bool) tutor_utils()->get_option( 'instructor_can_publish_course', false );
1072 if ( ! User::is_admin() && ! $is_instructor_allowed_to_publish ) {
1073 $params['post_status'] = CourseModel::STATUS_PENDING;
1074 }
1075 }
1076
1077 $is_error = apply_filters( 'tutor_is_error_before_course_update', false, $params );
1078 if ( is_wp_error( $is_error ) ) {
1079 $this->response_bad_request( $is_error->get_error_message() );
1080 }
1081
1082 $params['ID'] = $course_id;
1083 $update_id = wp_update_post( $params, true );
1084 if ( is_wp_error( $update_id ) ) {
1085 $this->json_response( $update_id->get_error_message(), null, HttpHelper::STATUS_INTERNAL_SERVER_ERROR );
1086 }
1087
1088 $this->setup_course_categories_tags( $update_id, $params );
1089 $this->prepare_update_post_meta( $params );
1090
1091 // Update course thumb.
1092 $thumbnail_id = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
1093 if ( $thumbnail_id ) {
1094 set_post_thumbnail( $update_id, $thumbnail_id );
1095 } else {
1096 delete_post_meta( $update_id, '_thumbnail_id' );
1097 }
1098
1099 $this->json_response(
1100 __( 'Course updated successfully.', 'tutor' ),
1101 $update_id,
1102 HttpHelper::STATUS_OK
1103 );
1104 }
1105
1106 /**
1107 * Unlink page builder from editor.
1108 *
1109 * @since 3.0.0
1110 *
1111 * @return void
1112 */
1113 public function ajax_unlink_page_builder() {
1114 tutor_utils()->check_nonce();
1115
1116 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1117 $builder = Input::post( 'builder' );
1118 $this->check_access( $course_id );
1119
1120 if ( 'elementor' === $builder ) {
1121 delete_post_meta( $course_id, '_elementor_edit_mode' );
1122 } elseif ( 'droip' === $builder ) {
1123 delete_post_meta( $course_id, 'droip_editor_mode' );
1124 }
1125
1126 $this->json_response(
1127 __( 'Builder unlinked successfully.', 'tutor' ),
1128 $course_id,
1129 HttpHelper::STATUS_OK
1130 );
1131 }
1132
1133 /**
1134 * Get all course contents by course id.
1135 *
1136 * @since 3.0.0
1137 *
1138 * @param int $course_id course id.
1139 *
1140 * @return array
1141 */
1142 public function get_course_contents( $course_id ) {
1143 $data = array();
1144 $topics = tutor_utils()->get_topics( $course_id );
1145
1146 if ( $topics->have_posts() ) {
1147 foreach ( $topics->get_posts() as $post ) {
1148 $current_topic = array(
1149 'id' => $post->ID,
1150 'title' => $post->post_title,
1151 'summary' => $post->post_content,
1152 'contents' => array(),
1153 );
1154
1155 $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 );
1156
1157 if ( $topic_contents->have_posts() ) {
1158 foreach ( $topic_contents->get_posts() as $post ) {
1159 if ( tutor()->quiz_post_type === $post->post_type ) {
1160 $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1161 $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
1162 }
1163
1164 array_push( $current_topic['contents'], $post );
1165 }
1166 }
1167
1168 $current_topic = apply_filters( 'tutor_filter_course_content', $current_topic );
1169
1170 array_push( $data, $current_topic );
1171 }
1172 }
1173
1174 return $data;
1175 }
1176
1177 /**
1178 * Get course contents
1179 *
1180 * @since 3.0.0
1181 */
1182 public function ajax_course_contents() {
1183 tutor_utils()->check_nonce();
1184
1185 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1186
1187 $this->check_access( $course_id );
1188
1189 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1190 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1191 }
1192
1193 if ( ! empty( $errors ) ) {
1194 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1195 }
1196
1197 $contents = $this->get_course_contents( $course_id );
1198
1199 $this->json_response(
1200 __( 'Course contents fetched successfully', 'tutor' ),
1201 $contents
1202 );
1203 }
1204
1205 /**
1206 * Get course details by ID
1207 *
1208 * @since 3.0.0
1209 *
1210 * @return void
1211 */
1212 public function ajax_course_details() {
1213 tutor_utils()->check_nonce();
1214
1215 $errors = array();
1216 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1217
1218 $this->check_access( $course_id );
1219
1220 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1221 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1222 }
1223
1224 if ( ! empty( $errors ) ) {
1225 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1226 }
1227
1228 $price_type = tutor_utils()->price_type( $course_id );
1229 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1230
1231 $product_name = '';
1232 $price = 0;
1233 $sale_price = 0;
1234 $product_id = tutor_utils()->get_course_product_id( $course_id );
1235
1236 if ( 'wc' === $monetize_by ) {
1237 $product = wc_get_product( $product_id );
1238 if ( $product ) {
1239 $product_name = $product->get_name();
1240 $price = $product->get_regular_price();
1241 $sale_price = $product->get_sale_price();
1242 }
1243 }
1244
1245 if ( 'tutor' === $monetize_by ) {
1246 $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1247 $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1248 }
1249
1250 $course_pricing = array(
1251 'type' => $price_type,
1252 'product_id' => $product_id,
1253 'product_name' => $product_name,
1254 'price' => $price,
1255 'sale_price' => $sale_price,
1256 );
1257
1258 $video_intro = get_post_meta( $course_id, '_video', true );
1259 if ( $video_intro ) {
1260 $source = $video_intro['source'] ?? '';
1261 if ( 'html5' === $source ) {
1262 $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1263 $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1264 $video['poster_url'] = $poster_url;
1265 $video['source_html5'] = $source_html5;
1266 }
1267 }
1268
1269 $course = get_post( $course_id, ARRAY_A );
1270 if ( $course ) {
1271 $course['post_name'] = urldecode( $course['post_name'] );
1272 }
1273
1274 $editors = tutor_utils()->get_editor_list( $course_id );
1275
1276 $data = array(
1277 'editors' => array_values( $editors ),
1278 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1279 'preview_link' => get_preview_post_link( $course_id ),
1280 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1281 'course_categories' => wp_get_post_terms( $course_id, CourseModel::COURSE_CATEGORY ),
1282 'course_tags' => wp_get_post_terms( $course_id, CourseModel::COURSE_TAG ),
1283 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1284 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
1285
1286 'enable_qna' => get_post_meta( $course_id, '_tutor_enable_qa', true ),
1287 'is_public_course' => get_post_meta( $course_id, '_tutor_is_public_course', true ),
1288 'course_level' => get_post_meta( $course_id, '_tutor_course_level', true ),
1289 'video' => $video_intro,
1290 'course_duration' => get_post_meta( $course_id, '_course_duration', true ),
1291 'course_benefits' => get_post_meta( $course_id, '_tutor_course_benefits', true ),
1292 'course_requirements' => get_post_meta( $course_id, '_tutor_course_requirements', true ),
1293 'course_target_audience' => get_post_meta( $course_id, '_tutor_course_target_audience', true ),
1294 'course_material_includes' => get_post_meta( $course_id, '_tutor_course_material_includes', true ),
1295 'monetize_by' => $monetize_by,
1296 'course_pricing' => $course_pricing,
1297 'course_settings' => get_post_meta( $course_id, '_tutor_course_settings', true ),
1298 'step_completion_status' => array(
1299 'basic' => true,
1300 'curriculum' => false,
1301 'additional' => false,
1302 'certificate' => false,
1303 ),
1304 );
1305
1306 $data = apply_filters( 'tutor_course_details_response', array_merge( $course, $data ) );
1307
1308 $this->json_response( __( 'Data retrieved successfully!', 'tutor' ), $data );
1309 }
1310
1311 /**
1312 * Load course builder.
1313 *
1314 * @since 3.0.0
1315 *
1316 * @return void
1317 */
1318 public function load_course_builder() {
1319 global $pagenow;
1320
1321 $has_pro = tutor()->has_pro;
1322 $has_access_role = User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) );
1323
1324 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1325 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1326 $backend_edit = $backend_builder && $course_id;
1327
1328 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1329 $frontend_edit = $is_frontend_builder && $course_id;
1330
1331 if ( $has_access_role && ( $backend_edit || ( $has_pro && $frontend_edit ) ) ) {
1332 $post_type = get_post_type( $course_id );
1333 $can_edit_course = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
1334
1335 if ( tutor()->course_post_type === $post_type && ( User::is_admin() || $can_edit_course ) ) {
1336 /**
1337 * Edit trash course behavior
1338 *
1339 * @since 3.0.0
1340 */
1341 if ( CourseModel::STATUS_TRASH === get_post_status( $course_id ) ) {
1342 $message = User::is_admin()
1343 ? __( 'You cannot edit this course because it is in the Trash. Please restore it and try again', 'tutor' )
1344 : tutor_utils()->error_message();
1345 wp_die( esc_html( $message ) );
1346 }
1347
1348 $this->load_course_builder_view();
1349 }
1350 }
1351 }
1352
1353 /**
1354 * Enqueue course builder assets like CSS, JS
1355 *
1356 * @since 3.0.0
1357 *
1358 * @return void
1359 */
1360 public function enqueue_course_builder_assets() {
1361 // Fix: function print_emoji_styles is deprecated since version 6.4.0!
1362 remove_action( 'wp_print_styles', 'print_emoji_styles' );
1363 remove_action( 'wp_head', 'wp_admin_bar_header' );
1364 add_action( 'wp_head', 'wp_enqueue_admin_bar_header_styles' );
1365
1366 do_action( 'tutor_course_builder_before_wp_editor_load' );
1367 wp_enqueue_script( 'wp-tinymce' );
1368 wp_enqueue_script( 'mce-view' );
1369 wp_enqueue_editor();
1370
1371 wp_enqueue_media();
1372 wp_enqueue_script( 'tutor-course-builder', tutor()->url . 'assets/js/tutor-course-builder.min.js', array( 'wp-date', 'wp-i18n', 'wp-element', 'wp-api' ), TUTOR_VERSION, true );
1373
1374 wp_localize_script(
1375 'mce-view',
1376 'mceViewL10n',
1377 array(
1378 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1379 )
1380 );
1381 }
1382
1383 /**
1384 * Localize custom course builder data for _tutorobject.
1385 *
1386 * @since 3.3.1
1387 *
1388 * @param array $data the localized data.
1389 *
1390 * @return array
1391 */
1392 public function localize_course_builder_data( $data ) {
1393 global $pagenow;
1394
1395 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1396 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1397 $backend_edit = $backend_builder && $course_id;
1398
1399 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1400 $frontend_edit = $is_frontend_builder && $course_id;
1401
1402 if ( ! $backend_edit && ! $frontend_edit ) {
1403 return $data;
1404 }
1405
1406 /**
1407 * Prepare course builder data.
1408 */
1409 $default_data = ( new Assets( false ) )->get_default_localized_data();
1410
1411 if ( isset( $default_data['current_user']['data']['id'] ) ) {
1412 $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1413 $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
1414 }
1415
1416 /**
1417 * Localized only options to protect sensitive info like API keys.
1418 */
1419 $required_options = array(
1420 'monetize_by',
1421 'enable_course_marketplace',
1422 'course_permalink_base',
1423 'supported_video_sources',
1424 'enrollment_expiry_enabled',
1425 'enable_q_and_a_on_course',
1426 'instructor_can_delete_course',
1427 'chatgpt_enable',
1428 'hide_admin_bar_for_users',
1429 'enable_redirect_on_course_publish_from_frontend',
1430 'instructor_can_publish_course',
1431 'instructor_can_change_course_author',
1432 'instructor_can_manage_co_instructors',
1433 );
1434
1435 $full_settings = get_option( 'tutor_option', array() );
1436 $settings = Options_V2::get_only( $required_options );
1437 $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1438 $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1439 $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1440
1441 $new_data = array( 'settings' => $settings );
1442
1443 $data = array_merge( $default_data, $new_data );
1444
1445 /**
1446 * Course builder dashboard URL based on role and settings.
1447 */
1448 $dashboard_url = tutor_utils()->tutor_dashboard_url();
1449 if ( User::is_admin() ) {
1450 $dashboard_url = get_admin_url();
1451 }
1452
1453 /**
1454 * EDD product list
1455 */
1456 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1457 if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1458 $data['edd_products'] = tutor_utils()->get_edd_products();
1459 }
1460
1461 $difficulty_levels = array();
1462 foreach ( tutor_utils()->course_levels() as $value => $label ) {
1463 $difficulty_levels[] = array(
1464 'label' => $label,
1465 'value' => $value,
1466 );
1467 }
1468
1469 $supported_video_sources = array();
1470 $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1471
1472 foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1473 if ( in_array( $value, $saved_video_source_list, true ) ) {
1474 $supported_video_sources[] = array(
1475 'label' => $label,
1476 'value' => $value,
1477 );
1478 }
1479 }
1480
1481 $data['dashboard_url'] = $dashboard_url;
1482 $data['backend_course_list_url'] = get_admin_url( null, 'admin.php?page=tutor' );
1483 $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1484 $data['timezones'] = tutor_global_timezone_lists();
1485 $data['difficulty_levels'] = $difficulty_levels;
1486 $data['supported_video_sources'] = $supported_video_sources;
1487 $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1488 $data['max_upload_size'] = size_format( wp_max_upload_size() );
1489
1490 $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1491
1492 return $data;
1493 }
1494
1495 /**
1496 * Load view for course builder.
1497 *
1498 * @since 3.0.0
1499 *
1500 * @return void
1501 */
1502 public function load_course_builder_view() {
1503 /**
1504 * Hide admin menu and footer.
1505 *
1506 * @since 3.3.0
1507 */
1508 echo '<style>
1509 #adminmenumain, #wpfooter, .notice, #tutor-page-wrap { display: none !important; }
1510 #wpcontent { margin: 0 !important; padding: 0 !important; }
1511 #wpbody-content { padding-bottom: 0px !important; float: none; }
1512 </style>';
1513
1514 do_action( 'tutor_before_course_builder_load' );
1515 include_once tutor()->path . 'views/pages/course-builder.php';
1516 do_action( 'tutor_after_course_builder_load' );
1517 }
1518
1519 /**
1520 * Add enroll require login class
1521 *
1522 * @since 2.6.0
1523 *
1524 * @param string $class_name css class name.
1525 *
1526 * @return string
1527 */
1528 public function add_enroll_required_login_class( $class_name ) {
1529 $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1530 if ( ! $enabled_tutor_login ) {
1531 return '';
1532 }
1533
1534 return $class_name;
1535 }
1536
1537 /**
1538 * Get list of WC products.
1539 *
1540 * @since 2.5.0
1541 * @since 3.0.0 exclude_linked_products, course_id are added.
1542 *
1543 * @return void
1544 */
1545 public function get_wc_products() {
1546 $exclude = array();
1547 $exclude_linked_products = Input::has( 'exclude_linked_products' );
1548 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1549
1550 if ( $exclude_linked_products ) {
1551 $exclude = tutor_utils()->get_linked_product_ids();
1552 }
1553
1554 if ( $course_id ) {
1555 $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1556 if ( $linked_product_id ) {
1557 $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1558 }
1559 }
1560
1561 $exclude = array_unique( $exclude );
1562
1563 $this->json_response(
1564 __( 'Products retrieved successfully!', 'tutor' ),
1565 tutor_utils()->get_wc_products_db( $exclude ),
1566 HttpHelper::STATUS_OK
1567 );
1568 }
1569
1570 /**
1571 * Get course associate WC product info by Ajax request
1572 *
1573 * @since 2.0.7
1574 *
1575 * @return void
1576 */
1577 public function get_wc_product() {
1578 tutor_utils()->checking_nonce();
1579 $product_id = Input::post( 'product_id' );
1580 $product = wc_get_product( $product_id );
1581 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1582
1583 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1584
1585 /**
1586 * If selected product is already linked with
1587 * a course & it is not the current course the
1588 * return error
1589 *
1590 * @since 2.1.0
1591 */
1592 if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1593 wp_send_json_error(
1594 __( 'One product can not be added to multiple course!', 'tutor' )
1595 );
1596 }
1597
1598 if ( $product ) {
1599 $data = array(
1600 'name' => $product->get_name(),
1601 'regular_price' => $product->get_regular_price(),
1602 'sale_price' => $product->get_sale_price(),
1603 );
1604 wp_send_json_success( $data );
1605 } else {
1606 wp_send_json_error( __( 'Product not found', 'tutor' ) );
1607 }
1608 }
1609
1610 /**
1611 * Update course content order
1612 *
1613 * @since 1.0.0
1614 * @return void
1615 */
1616 public function tutor_update_course_content_order() {
1617 tutor_utils()->checking_nonce();
1618
1619 if ( Input::has( 'content_parent' ) ) {
1620 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1621 $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1622 $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1623
1624 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1625 wp_send_json_success( array( 'message' => __( 'Access Denied!', 'tutor' ) ) );
1626 exit;
1627 }
1628
1629 // Update the parent topic id of the content.
1630 global $wpdb;
1631 $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1632 }
1633
1634 // Save course content order.
1635 $this->save_course_content_order();
1636
1637 wp_send_json_success();
1638 }
1639
1640 /**
1641 * Restrict new student entry
1642 *
1643 * @since 1.0.0
1644 * @param mixed $content content.
1645 *
1646 * @return mixed
1647 */
1648 public function restrict_new_student_entry( $content ) {
1649
1650 if ( ! tutor_utils()->is_course_fully_booked() ) {
1651 // No restriction if not fully booked.
1652 return $content;
1653 }
1654
1655 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">' .
1656 __( 'Fully Booked', 'tutor' )
1657 . '</div></div>';
1658 }
1659
1660 /**
1661 * Restrict media
1662 *
1663 * @since 1.0.0
1664 * @param string $where where clause.
1665 * @return string
1666 */
1667 public function restrict_media( $where ) {
1668 $action = Input::post( 'action' );
1669 if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1670 if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1671 $where .= ' AND post_author=' . get_current_user_id();
1672 }
1673 }
1674
1675 return $where;
1676 }
1677
1678 /**
1679 * Save course content order
1680 *
1681 * @since 1.0.0
1682 * @return void
1683 */
1684 private function save_course_content_order() {
1685 global $wpdb;
1686
1687 $new_order = Input::post( 'tutor_topics_lessons_sorting' );
1688 if ( ! empty( $new_order ) ) {
1689 $order = json_decode( $new_order, true );
1690
1691 if ( is_array( $order ) && count( $order ) ) {
1692 $i = 0;
1693 foreach ( $order as $topic ) {
1694 $i++;
1695 $wpdb->update(
1696 $wpdb->posts,
1697 array( 'menu_order' => $i ),
1698 array( 'ID' => $topic['topic_id'] )
1699 );
1700
1701 /**
1702 * Removing All lesson with topic
1703 */
1704
1705 $wpdb->update(
1706 $wpdb->posts,
1707 array( 'post_parent' => 0 ),
1708 array( 'post_parent' => $topic['topic_id'] )
1709 );
1710
1711 /**
1712 * Lesson Attaching with topic ID
1713 * Sorting lesson
1714 */
1715 if ( isset( $topic['lesson_ids'] ) ) {
1716 $lesson_ids = $topic['lesson_ids'];
1717 } else {
1718 $lesson_ids = array();
1719 }
1720 if ( count( $lesson_ids ) ) {
1721 foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1722 $wpdb->update(
1723 $wpdb->posts,
1724 array(
1725 'post_parent' => $topic['topic_id'],
1726 'menu_order' => $lesson_key,
1727 ),
1728 array( 'ID' => $lesson_id )
1729 );
1730 }
1731 }
1732 }
1733 }
1734 }
1735 }
1736
1737 /**
1738 * Insert Topic and attached it with Course
1739 *
1740 * @since 1.0.0
1741 *
1742 * @param integer $post_ID post ID.
1743 * @param object $post post object.
1744 *
1745 * @return void
1746 */
1747 public function save_course_meta( $post_ID, $post ) {
1748 global $wpdb;
1749
1750 do_action( 'tutor_save_course', $post_ID, $post );
1751
1752 /**
1753 * Save course price type
1754 */
1755 $price_type = Input::post( 'tutor_course_price_type' );
1756 if ( $price_type ) {
1757 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
1758 }
1759
1760 //phpcs:disable WordPress.Security.NonceVerification.Missing
1761 // Course Duration.
1762 if ( ! empty( $_POST['course_duration'] ) ) {
1763 $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1764 update_post_meta( $post_ID, '_course_duration', $video );
1765 }
1766
1767 if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1768 $course_level = Input::post( '_tutor_course_level' );
1769 update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1770 }
1771
1772 $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1773 if ( $additional_data_edit ) {
1774 if ( ! empty( $_POST['course_benefits'] ) ) {
1775 $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1776 update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1777 } elseif ( ! tutor_is_rest() ) {
1778 delete_post_meta( $post_ID, '_tutor_course_benefits' );
1779 }
1780
1781 if ( ! empty( $_POST['course_requirements'] ) ) {
1782 $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1783 update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1784 } elseif ( ! tutor_is_rest() ) {
1785 delete_post_meta( $post_ID, '_tutor_course_requirements' );
1786 }
1787
1788 if ( ! empty( $_POST['course_target_audience'] ) ) {
1789 $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1790 update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1791 } elseif ( ! tutor_is_rest() ) {
1792 delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1793 }
1794
1795 if ( ! empty( $_POST['course_material_includes'] ) ) {
1796 $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1797 update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1798 } elseif ( ! tutor_is_rest() ) {
1799 delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1800 }
1801 //phpcs:enable WordPress.Security.NonceVerification.Missing
1802 }
1803
1804 /**
1805 * Sorting Topics and lesson
1806 */
1807 $this->save_course_content_order();
1808
1809 // Additional data like course intro video.
1810 if ( $additional_data_edit ) {
1811 // Sanitize data through helper method.
1812 $video = Input::sanitize_array(
1813 $_POST['video'] ?? array(), //phpcs:ignore
1814 array(
1815 'source_external_url' => 'esc_url',
1816 'source_embedded' => 'wp_kses_post',
1817 ),
1818 true
1819 );
1820 $video_source = tutor_utils()->array_get( 'source', $video );
1821 if ( -1 !== $video_source ) {
1822 update_post_meta( $post_ID, '_video', $video );
1823 } elseif ( ! tutor_is_rest() ) {
1824 delete_post_meta( $post_ID, '_video' );
1825 }
1826 }
1827
1828 /**
1829 * Adding author to instructor automatically
1830 */
1831
1832 // Override post author id.
1833 $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1834 $attached = (int) $wpdb->get_var(
1835 $wpdb->prepare(
1836 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1837 WHERE user_id = %d
1838 AND meta_key = '_tutor_instructor_course_id'
1839 AND meta_value = %d ",
1840 $author_id,
1841 $post_ID
1842 )
1843 );
1844
1845 if ( ! $attached ) {
1846 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1847 }
1848
1849 /**
1850 * Disable question and answer for this course
1851 *
1852 * @since 1.7.0
1853 */
1854 if ( $additional_data_edit ) {
1855 foreach ( $this->additional_meta as $key ) {
1856 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1857 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1858 }
1859 }
1860
1861 do_action( 'tutor_save_course_after', $post_ID, $post );
1862 }
1863
1864 /**
1865 * Save course topic
1866 *
1867 * @since 1.0.0
1868 * @since 3.0.0 response and input name updated.
1869 *
1870 * @return void
1871 */
1872 public function tutor_save_topic() {
1873 tutor_utils()->check_nonce();
1874
1875 $is_update = false;
1876 $errors = array();
1877 $topic_title = Input::post( 'title' );
1878
1879 if ( empty( $topic_title ) ) {
1880 $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
1881 $this->json_response(
1882 __( 'Invalid inputs', 'tutor' ),
1883 $errors,
1884 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
1885 );
1886 }
1887
1888 // Gather parameters.
1889 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1890 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1891 $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
1892
1893 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
1894
1895 // Validate if user can manage the topic.
1896 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
1897 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1898 }
1899
1900 // Create payload to create/update the topic.
1901 $post_arr = array(
1902 'post_type' => 'topics',
1903 'post_title' => $topic_title,
1904 'post_content' => $topic_summary,
1905 'post_status' => 'publish',
1906 'post_author' => get_current_user_id(),
1907 'post_parent' => $course_id,
1908 'menu_order' => $next_topic_order_id,
1909 );
1910
1911 if ( $topic_id ) {
1912 $is_update = true;
1913 $post_arr['ID'] = $topic_id;
1914 }
1915
1916 $current_topic_id = wp_insert_post( $post_arr );
1917
1918 if ( $is_update ) {
1919 $this->json_response(
1920 __( 'Topic updated successfully!', 'tutor' ),
1921 $current_topic_id
1922 );
1923 } else {
1924 $this->json_response(
1925 __( 'Topic created successfully!', 'tutor' ),
1926 $current_topic_id,
1927 HttpHelper::STATUS_CREATED
1928 );
1929 }
1930 }
1931
1932 /**
1933 * Delete a course topic
1934 *
1935 * @since 1.0.0
1936 * @since 3.0.0 code refactor and response updated.
1937 *
1938 * @return void
1939 */
1940 public function tutor_delete_topic() {
1941 tutor_utils()->check_nonce();
1942
1943 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1944 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1945 $this->json_response(
1946 tutor_utils()->error_message(),
1947 null,
1948 HttpHelper::STATUS_FORBIDDEN
1949 );
1950 }
1951
1952 global $wpdb;
1953
1954 // Assign course ID to orphan content IDs since the topic will be deleted.
1955 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
1956 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
1957 foreach ( $content_ids as $content_id ) {
1958 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
1959 // Actually all kind of contents.
1960 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
1961 }
1962
1963 // Set contents under the topic orphan.
1964 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
1965
1966 // Then delete the topic from database.
1967 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
1968 wp_delete_post( $topic_id );
1969
1970 $this->json_response(
1971 __( 'Topic deleted successfully!', 'tutor' )
1972 );
1973 }
1974
1975 /**
1976 * Handle enroll now action
1977 *
1978 * @since 1.0.0
1979 *
1980 * @return void
1981 */
1982 public function enroll_now() {
1983
1984 if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
1985 return;
1986 }
1987
1988 // Checking Nonce.
1989 tutor_utils()->checking_nonce();
1990
1991 $user_id = get_current_user_id();
1992 if ( ! $user_id ) {
1993 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
1994 }
1995
1996 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
1997
1998 /**
1999 * TODO: need to check purchase information
2000 */
2001
2002 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2003
2004 /**
2005 * If is is not purchasable, it's free, and enroll right now
2006 * If purchasable, then process purchase.
2007 *
2008 * @since: v.1.0.0
2009 */
2010 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2011 // Process purchase.
2012
2013 } else {
2014 // Free enroll.
2015 tutor_utils()->do_enroll( $course_id );
2016 }
2017
2018 $referer_url = wp_get_referer();
2019 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2020 exit;
2021 }
2022
2023 /**
2024 * Mark complete completed
2025 *
2026 * @since 1.0.0
2027 * @return void
2028 */
2029 public function mark_course_complete() {
2030 $tutor_action = Input::post( 'tutor_action' );
2031 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2032 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
2033 return;
2034 }
2035
2036 // Checking nonce.
2037 tutor_utils()->checking_nonce();
2038
2039 $user_id = get_current_user_id();
2040
2041 // TODO: need to show view if not signed_in.
2042 if ( ! $user_id ) {
2043 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2044 }
2045
2046 CourseModel::mark_course_as_completed( $course_id, $user_id );
2047
2048 $permalink = get_the_permalink( $course_id );
2049
2050 // Set temporary identifier to show review pop up.
2051 self::set_review_popup_data( $user_id, $course_id, $permalink );
2052
2053 wp_safe_redirect( $permalink );
2054 exit;
2055 }
2056
2057 /**
2058 * Set data for review popup.
2059 *
2060 * @since 2.2.5
2061 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2062 *
2063 * @param int $user_id user id.
2064 * @param int $course_id course id.
2065 *
2066 * @return void
2067 */
2068 public static function set_review_popup_data( $user_id, $course_id ) {
2069 if ( get_tutor_option( 'enable_course_review' ) ) {
2070 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2071 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2072 $meta_key = User::get_review_popup_meta( $course_id );
2073 add_user_meta( $user_id, $meta_key, $course_id, true );
2074 }
2075 }
2076 }
2077
2078 /**
2079 * Popup review form on course details
2080 *
2081 * @since 1.0.0
2082 * @return void
2083 */
2084 public function popup_review_form() {
2085 if ( is_user_logged_in() ) {
2086 $user_id = get_current_user_id();
2087 $course_id = get_the_ID();
2088 $meta_key = User::get_review_popup_meta( $course_id );
2089 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2090
2091 if ( is_single() && $course_id === $review_course_id ) {
2092 include tutor()->path . 'views/modal/review.php';
2093 }
2094 }
2095 }
2096
2097 /**
2098 * Review popup data clear
2099 *
2100 * @since 2.4.0
2101 *
2102 * @return void
2103 */
2104 public function clear_review_popup_data() {
2105 tutils()->checking_nonce();
2106
2107 if ( is_user_logged_in() ) {
2108 $user_id = get_current_user_id();
2109 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2110
2111 if ( $course_id ) {
2112 $meta_key = User::get_review_popup_meta( $course_id );
2113 delete_user_meta( $user_id, $meta_key, $course_id );
2114 }
2115
2116 wp_send_json_success();
2117 }
2118 }
2119
2120 /**
2121 * Delete course delete from frontend dashboard
2122 *
2123 * @since 2.0.0
2124 * @return void
2125 */
2126 public function tutor_delete_dashboard_course() {
2127 tutor_utils()->checking_nonce();
2128
2129 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2130 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2131 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2132 }
2133
2134 /**
2135 * Co-instructor can not delete a course
2136 *
2137 * @since 2.1.6
2138 */
2139 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2140 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2141 }
2142
2143 // Check if user is only an instructor.
2144 if ( ! current_user_can( 'administrator' ) ) {
2145 // Check if instructor can trash course.
2146 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2147
2148 if ( ! $can_trash_post ) {
2149 wp_send_json_error( tutor_utils()->error_message() );
2150 }
2151 }
2152
2153 $trash_course = wp_update_post(
2154 array(
2155 'ID' => $course_id,
2156 'post_status' => 'trash',
2157 )
2158 );
2159
2160 if ( $trash_course ) {
2161 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2162 }
2163 wp_send_json_success();
2164 }
2165
2166 /**
2167 * Main author change from gutenberg editor
2168 *
2169 * @since 2.0.0
2170 *
2171 * @param array $data data.
2172 * @param array $postarr post array.
2173 *
2174 * @return mixed
2175 */
2176 public function tutor_add_gutenberg_author( $data, $postarr ) {
2177 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2178 $post_type = $postarr['post_type'];
2179 $courses_post_type = tutor()->course_post_type;
2180
2181 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2182 return $data;
2183 }
2184
2185 /**
2186 * Only admin can change main author
2187 */
2188 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2189 global $wpdb;
2190 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2191 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2192
2193 if ( $post_author > 0 ) {
2194 $data['post_author'] = $post_author;
2195 } else {
2196 $data['post_author'] = get_current_user_id();
2197 }
2198 }
2199
2200 return $data;
2201 }
2202
2203
2204 /**
2205 * Attach product with course when course save from frontend or backend.
2206 *
2207 * @since 1.3.4
2208 *
2209 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2210 *
2211 * @param integer $post_ID course ID.
2212 * @param array $post_data created course post details.
2213 *
2214 * @return void
2215 */
2216 public function attach_product_with_course( $post_ID, $post_data ) {
2217
2218 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2219 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2220
2221 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2222 return;
2223 }
2224
2225 /**
2226 * Unlink product from course.
2227 */
2228 if ( -1 === $product_id ) {
2229 delete_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META );
2230 return;
2231 }
2232
2233 /**
2234 * Free user can only select product from dropdown
2235 */
2236 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2237 if ( $product_id > 0 ) {
2238 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2239 }
2240
2241 return;
2242 }
2243
2244 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2245 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2246 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2247
2248 if ( ! $course_price || $sale_price >= $course_price ) {
2249 return;
2250 }
2251
2252 $course = get_post( $post_ID );
2253
2254 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2255
2256 if ( 'wc' === $monetize_by ) {
2257
2258 $is_update = ( $attached_product_id && wc_get_product( $attached_product_id ) ) ? true : false;
2259
2260 if ( $is_update ) {
2261 $attached_product_id = $product_id;
2262 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2263
2264 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $attached_product_id );
2265 $product_obj = wc_get_product( $product_id );
2266 if ( $product_obj->is_type( 'subscription' ) ) {
2267 update_post_meta( $attached_product_id, '_subscription_price', $course_price );
2268 }
2269
2270 // Set course regular & sale price.
2271 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2272 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2273 } else {
2274 // Create new WC product name with course title.
2275 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2276 if ( $product_id ) {
2277 $product_obj = wc_get_product( $product_id );
2278 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2279 // Mark product for woocommerce.
2280 update_post_meta( $product_id, '_virtual', 'yes' );
2281 update_post_meta( $product_id, '_tutor_product', 'yes' );
2282
2283 // Set course regular & sale price.
2284 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2285 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2286 }
2287 }
2288
2289 $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2290 if ( $product_id && $course_post_thumbnail ) {
2291 set_post_thumbnail( $product_id, $course_post_thumbnail );
2292 }
2293 } elseif ( 'edd' === $monetize_by ) {
2294
2295 $is_update = false;
2296
2297 if ( $attached_product_id ) {
2298 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2299 if ( $edd_price ) {
2300 $is_update = true;
2301 }
2302 }
2303
2304 if ( $is_update ) {
2305 // Update the product.
2306 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2307 } else {
2308 // Create new product.
2309
2310 $post_arr = array(
2311 'post_type' => 'download',
2312 'post_title' => $course->post_title,
2313 'post_status' => 'publish',
2314 'post_author' => get_current_user_id(),
2315 );
2316 $download_id = wp_insert_post( $post_arr );
2317 if ( $download_id ) {
2318 // EDD edd_price.
2319 update_post_meta( $download_id, 'edd_price', $course_price );
2320
2321 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2322 // Mark product for EDD.
2323 update_post_meta( $download_id, '_tutor_product', 'yes' );
2324
2325 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2326 if ( $course_post_thumbnail ) {
2327 set_post_thumbnail( $download_id, $course_post_thumbnail );
2328 }
2329 }
2330 }
2331 }
2332 }
2333
2334 /**
2335 * Add Course level to course settings
2336 *
2337 * @since 1.4.1
2338 *
2339 * @param array $args arguments.
2340 * @return array
2341 */
2342 public function add_course_level_to_settings( $args ) {
2343 $course_id = get_the_ID();
2344 $levels = tutor_utils()->course_levels();
2345 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2346
2347 $args['general']['fields']['_tutor_course_level'] = array(
2348 'type' => 'select',
2349 'label' => __( 'Difficulty Level', 'tutor' ),
2350 'label_title' => __( 'Enable', 'tutor' ),
2351 'options' => $levels,
2352 'value' => $course_level ? $course_level : 'intermediate',
2353 'desc' => __( 'Course difficulty level', 'tutor' ),
2354 );
2355
2356 return $args;
2357 }
2358
2359 /**
2360 * Check if course starting
2361 *
2362 * @since 1.4.8
2363 * @return void
2364 */
2365 public function tutor_lesson_load_before() {
2366 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2367 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2368 if ( is_user_logged_in() ) {
2369 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2370 if ( ! $completed_lessons && ! $is_course_started ) {
2371 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2372 do_action( 'tutor/course/started', $course_id );
2373 }
2374 }
2375 }
2376
2377 /**
2378 * Add Course level to course settings
2379 *
2380 * @since 1.4.8
2381 * @return void
2382 */
2383 public function course_elements_enable_disable() {
2384 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2385 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2386 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2387 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2388 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2389 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2390 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2391 }
2392
2393 /**
2394 * Enable disable course progress bar
2395 *
2396 * @since 1.4.8
2397 *
2398 * @param string $html HTML string.
2399 * @return string
2400 */
2401 public function enable_disable_course_progress_bar( $html ) {
2402 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2403 if ( $disable_option ) {
2404 return '';
2405 }
2406 return $html;
2407 }
2408
2409 /**
2410 * Enable disable material includes
2411 *
2412 * @since 1.4.8
2413 *
2414 * @param string $html HTML string.
2415 * @return string
2416 */
2417 public function enable_disable_material_includes( $html ) {
2418 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2419 if ( $disable_option ) {
2420 return '';
2421 }
2422 return $html;
2423 }
2424
2425 /**
2426 * Enable disable course content
2427 *
2428 * @since 1.4.8
2429 *
2430 * @param string $html HTML string.
2431 * @return string
2432 */
2433 public function enable_disable_course_content( $html ) {
2434 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2435 if ( $disable_option ) {
2436 return '';
2437 }
2438 return $html;
2439 }
2440
2441 /**
2442 * Enable disable course benefits
2443 *
2444 * @since 1.4.8
2445 *
2446 * @param string $html HTML string.
2447 * @return string
2448 */
2449 public function enable_disable_course_benefits( $html ) {
2450 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2451 if ( $disable_option ) {
2452 return '';
2453 }
2454 return $html;
2455 }
2456
2457 /**
2458 * Enable disable course requirements
2459 *
2460 * @since 1.4.8
2461 *
2462 * @param string $html HTML string.
2463 * @return string
2464 */
2465 public function enable_disable_course_requirements( $html ) {
2466 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2467 if ( $disable_option ) {
2468 return '';
2469 }
2470 return $html;
2471 }
2472
2473 /**
2474 * Enable disable course target audience
2475 *
2476 * @since 1.4.8
2477 *
2478 * @param string $html HTML string.
2479 * @return string
2480 */
2481 public function enable_disable_course_target_audience( $html ) {
2482 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2483 if ( $disable_option ) {
2484 return '';
2485 }
2486 return $html;
2487 }
2488
2489 /**
2490 * Enable disable course nav items
2491 *
2492 * @since 1.4.8
2493 *
2494 * @param array $items item list.
2495 * @param integer $course_id course ID.
2496 *
2497 * @return array
2498 */
2499 public function enable_disable_course_nav_items( $items, $course_id ) {
2500 global $wp_query, $post;
2501 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2502 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2503 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2504
2505 // Whether Q&A enabled.
2506 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2507 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2508 unset( $items['questions'] );
2509 }
2510 }
2511
2512 // Whether announcment enabled.
2513 if ( $disable_course_announcements ) {
2514 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2515 unset( $items['announcements'] );
2516 }
2517 }
2518
2519 // Hide review section if disabled.
2520 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2521 unset( $items['reviews'] );
2522 }
2523
2524 // Whether enrollment require.
2525 $is_enrolled = tutor_utils()->is_enrolled();
2526
2527 return array_filter(
2528 $items,
2529 function ( $item ) use ( $is_enrolled ) {
2530 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2531 return $is_enrolled;
2532 }
2533 return true;
2534 }
2535 );
2536 }
2537
2538 /**
2539 * Filter product in shop page
2540 *
2541 * @since 1.4.9
2542 * @return void|null
2543 */
2544 public function filter_product_in_shop_page() {
2545 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2546 if ( ! $hide_course_from_shop_page ) {
2547 return;
2548 }
2549 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2550 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2551 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2552 }
2553
2554
2555 /**
2556 * Tutor product meta query
2557 *
2558 * @since 1.4.9
2559 * @return array
2560 */
2561 public function tutor_product_meta_query() {
2562 $meta_query = array(
2563 'key' => '_tutor_product',
2564 'compare' => 'NOT EXISTS',
2565 );
2566 return $meta_query;
2567 }
2568
2569 /**
2570 * Filter product in woocommerce shop page
2571 *
2572 * @since 1.4.9
2573 *
2574 * @param \WP_Query $wp_query WP Query instance.
2575 * @return \WP_Query
2576 */
2577 public function filter_woocommerce_product_query( $wp_query ) {
2578 $product_ids = $this->get_connected_wc_product_ids();
2579 $wp_query->set( 'post__not_in', $product_ids );
2580 return $wp_query;
2581 }
2582
2583 /**
2584 * Get connected woocommerce product ids for course and course bundle
2585 *
2586 * @since 2.7.2
2587 *
2588 * @return array
2589 */
2590 public function get_connected_wc_product_ids() {
2591 global $wpdb;
2592
2593 $results = $wpdb->get_results(
2594 $wpdb->prepare(
2595 "SELECT DISTINCT pm.meta_value product_id
2596 FROM {$wpdb->posts} p
2597 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2598 AND pm.meta_key = %s
2599 WHERE post_type IN( 'courses','course-bundle' )",
2600 '_tutor_course_product_id'
2601 )
2602 );
2603
2604 $ids = array();
2605 if ( is_array( $results ) && count( $results ) ) {
2606 $ids = array_column( $results, 'product_id' );
2607 }
2608
2609 return $ids;
2610 }
2611
2612 /**
2613 * Filter product in edd downloads shortcode page
2614 *
2615 * @since 1.4.9
2616 *
2617 * @param \WP_Query $query WP Query instance.
2618 * @return \WP_Query
2619 */
2620 public function filter_edd_downloads_query( $query ) {
2621 $query['meta_query'][] = $this->tutor_product_meta_query();
2622 return $query;
2623 }
2624
2625 /**
2626 * Filter product in edd downloads archive page
2627 *
2628 * @since 1.4.9
2629 *
2630 * @param \WP_Query $wp_query WP Query instance.
2631 * @return \WP_Query
2632 */
2633 public function filter_archive_meta_query( $wp_query ) {
2634 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2635 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2636 }
2637 return $wp_query;
2638 }
2639
2640 /**
2641 * Removed course price if already enrolled at single course
2642 *
2643 * @since 1.5.8
2644 *
2645 * @param string $html HTML string.
2646 * @return string
2647 */
2648 public function remove_price_if_enrolled( $html ) {
2649 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2650
2651 if ( $should_removed ) {
2652 $course_id = get_the_ID();
2653 $enrolled = tutor_utils()->is_enrolled( $course_id );
2654 if ( $enrolled ) {
2655 $html = '';
2656 }
2657 }
2658 return $html;
2659 }
2660
2661 /**
2662 * Check if all lessons and quizzes done before mark course complete.
2663 *
2664 * @since 1.5.8
2665 *
2666 * @param string $html HTML string.
2667 * @return string
2668 */
2669 public function tutor_lms_hide_course_complete_btn( $html ) {
2670
2671 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2672 if ( 'strict' !== $completion_mode ) {
2673 return $html;
2674 }
2675
2676 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2677 $lesson_count = tutor_utils()->get_lesson_count_by_course();
2678
2679 if ( $completed_lesson < $lesson_count ) {
2680 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2681 <div class="tutor-alert-text">
2682 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2683 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2684 </div>
2685 </div>';
2686 }
2687
2688 $quizzes = array();
2689 $assignments = array();
2690
2691 $course_contents = tutor_utils()->get_course_contents_by_id();
2692 if ( tutor_utils()->count( $course_contents ) ) {
2693 foreach ( $course_contents as $content ) {
2694 if ( 'tutor_quiz' === $content->post_type ) {
2695 $quizzes[] = $content;
2696 }
2697 if ( 'tutor_assignments' === $content->post_type ) {
2698 $assignments[] = $content;
2699 }
2700 }
2701 }
2702
2703 $required_assignment_pass = 0;
2704
2705 foreach ( $assignments as $row ) {
2706
2707 $submitted_assignment = tutor_utils()->is_assignment_submitted( $row->ID );
2708 $is_reviewed_by_instructor = null === $submitted_assignment
2709 ? false
2710 : get_comment_meta( $submitted_assignment->comment_ID, 'evaluate_time', true );
2711
2712 if ( $submitted_assignment && $is_reviewed_by_instructor ) {
2713 $pass_mark = tutor_utils()->get_assignment_option( $submitted_assignment->comment_post_ID, 'pass_mark' );
2714 $given_mark = get_comment_meta( $submitted_assignment->comment_ID, 'assignment_mark', true );
2715
2716 if ( $given_mark < $pass_mark ) {
2717 $required_assignment_pass++;
2718 }
2719 } else {
2720 $required_assignment_pass++;
2721 }
2722 }
2723
2724 $is_quiz_pass = true;
2725 $required_quiz_pass = 0;
2726
2727 if ( tutor_utils()->count( $quizzes ) ) {
2728 foreach ( $quizzes as $quiz ) {
2729
2730 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2731 if ( $attempt ) {
2732 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2733 $earned_percentage = $attempt->earned_marks > 0 ? ( number_format( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) ) : 0;
2734
2735 if ( $earned_percentage < $passing_grade ) {
2736 $required_quiz_pass++;
2737 $is_quiz_pass = false;
2738 }
2739 } else {
2740 $required_quiz_pass++;
2741 $is_quiz_pass = false;
2742 }
2743 }
2744 }
2745
2746 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2747 $_msg = '';
2748 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2749 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2750
2751 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2752 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2753 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2754 }
2755
2756 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2757 //phpcs:ignore
2758 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2759 }
2760
2761 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2762 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2763 $_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 );
2764 }
2765
2766 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2767 <div class="tutor-alert-text">
2768 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2769 <span>' . $_msg . '</span>
2770 </div>
2771 </div>';
2772 }
2773
2774 return $html;
2775 }
2776
2777 /**
2778 * Generate Gradebook
2779 *
2780 * @since 1.5.8
2781 *
2782 * @param string $html HTML string.
2783 * @return string
2784 */
2785 public function get_generate_greadbook( $html ) {
2786 if ( ! tutor_utils()->is_completed_course() ) {
2787 return '';
2788 }
2789 return $html;
2790 }
2791
2792 /**
2793 * Add social share content in header
2794 *
2795 * @since 1.6.3
2796 * @return void
2797 */
2798 public function social_share_content() {
2799 global $wp_query, $post;
2800 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2801 <!--Facebook-->
2802 <meta property="og:type" content="website"/>
2803 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2804 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2805 <!--Twitter-->
2806 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2807 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2808 <!--Google+-->
2809 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2810 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2811 <?php
2812 }
2813 }
2814
2815 /**
2816 * Delete associated enrollment
2817 *
2818 * @since 1.8.2
2819 *
2820 * @param integer $post_id post ID.
2821 * @return void
2822 */
2823 public function delete_associated_enrollment( $post_id ) {
2824 global $wpdb;
2825
2826 $enroll_id = $wpdb->get_var(
2827 $wpdb->prepare(
2828 "SELECT
2829 post_id
2830 FROM
2831 {$wpdb->postmeta}
2832 WHERE
2833 meta_key='_tutor_enrolled_by_order_id'
2834 AND meta_value = %d
2835 ",
2836 $post_id
2837 )
2838 );
2839
2840 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2841
2842 $course_id = get_post_field( 'post_parent', $enroll_id );
2843 $user_id = get_post_field( 'post_author', $enroll_id );
2844
2845 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2846 }
2847 }
2848
2849 /**
2850 * Reset course progress.
2851 *
2852 * @since 1.5.8
2853 * @return void
2854 */
2855 public function tutor_reset_course_progress() {
2856 tutor_utils()->checking_nonce();
2857 $course_id = Input::post( 'course_id' );
2858
2859 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2860 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2861 return;
2862 }
2863
2864 tutor_utils()->delete_course_progress( $course_id );
2865 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2866 }
2867
2868 /**
2869 * Do enroll if guest attempt to enroll and course is free
2870 *
2871 * @since 1.9.8
2872 *
2873 * @param integer $course_id course ID.
2874 * @param integer $user_id user ID.
2875
2876 * @return void
2877 */
2878 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
2879 $course_id = sanitize_text_field( $course_id );
2880 $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
2881
2882 if ( $course_id && $is_allowed ) {
2883 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2884 if ( ! $is_purchasable ) {
2885 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
2886 do_action( 'guest_attempt_after_enrollment', $course_id );
2887 }
2888 }
2889 }
2890
2891 /**
2892 * Handle course enrollment
2893 *
2894 * @since 2.1.0
2895 * @return void
2896 */
2897 public function course_enrollment() {
2898 tutor_utils()->checking_nonce();
2899
2900 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2901 $user_id = get_current_user_id();
2902
2903 if ( $course_id ) {
2904 $password_protected = post_password_required( $course_id );
2905 if ( $password_protected ) {
2906 wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
2907 }
2908 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
2909 if ( $enroll ) {
2910 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
2911 } else {
2912 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
2913 }
2914 } else {
2915 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
2916 }
2917 }
2918
2919 /**
2920 * After trash a course direct to the course list page
2921 *
2922 * @since 2.1.7
2923 *
2924 * @param integer $post_id int course id.
2925 *
2926 * @return void
2927 */
2928 public static function redirect_to_course_list_page( int $post_id ): void {
2929 $post = get_post( $post_id );
2930 if ( tutor()->course_post_type === $post->post_type ) {
2931 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2932 if ( ! $is_gutenberg_enabled ) {
2933 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
2934 exit;
2935 }
2936 }
2937 }
2938
2939 /**
2940 * Create or update WooCommerce product
2941 *
2942 * If product id not set it will create new one.
2943 *
2944 * @since 2.2.0
2945 *
2946 * @param string $title product title.
2947 * @param string $reg_price product price.
2948 * @param string $sale_price product sale price.
2949 * @param int $product_id product ID.
2950 * @param string $status product status.
2951 *
2952 * @return integer Product id or return 0 if WC not exists
2953 */
2954 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
2955 if ( ! tutor_utils()->has_wc() ) {
2956 return 0;
2957 }
2958
2959 $product_obj = new \WC_Product();
2960 if ( $product_id ) {
2961 $product_obj = wc_get_product( $product_id );
2962 }
2963
2964 $product_obj->set_name( $title );
2965 $product_obj->set_status( $status );
2966 $product_obj->set_price( $reg_price );
2967 $product_obj->set_regular_price( $reg_price );
2968
2969 if ( $sale_price > 0 ) {
2970 $product_obj->set_sale_price( $sale_price );
2971 } else {
2972 $product_obj->set_sale_price( null );
2973 }
2974
2975 $product_obj->set_sold_individually( true );
2976
2977 return $product_obj->save();
2978 }
2979
2980 /**
2981 * Get course/bundle mini info
2982 *
2983 * @since 3.0.0
2984 *
2985 * @param object $post Course or bundle post.
2986 *
2987 * @return array
2988 */
2989 public static function get_mini_info( object $post ) {
2990 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
2991 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
2992 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
2993 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
2994
2995 $info = array(
2996 'id' => $post->ID,
2997 'title' => $post->post_title,
2998 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
2999 'is_purchasable' => $is_purchasable,
3000 'regular_price' => $regular_price,
3001 'sale_price' => $sale_price,
3002 );
3003
3004 if ( 'course-bundle' === $post->post_type && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
3005 $info['total_course'] = count( BundleModel::get_bundle_course_ids( $post->ID ) );
3006 }
3007
3008 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
3009
3010 return $card_data;
3011 }
3012
3013 /**
3014 * Get course/bundle card data
3015 *
3016 * This method will return all data that contain in
3017 * course card
3018 *
3019 * @since 3.0.0
3020 *
3021 * @param object $post Course or bundle post.
3022 *
3023 * @return array
3024 */
3025 public static function get_card_data( object $post ) {
3026 $info = self::get_mini_info( $post );
3027
3028 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3029 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3030 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3031
3032 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
3033
3034 return $card_data;
3035 }
3036
3037 /**
3038 * Filter user list access for instructor
3039 *
3040 * @since 3.0.0
3041 *
3042 * @param bool $access access.
3043 *
3044 * @return bool
3045 */
3046 public function user_list_access_for_instructor( $access ) {
3047 $is_instructor = User::is_instructor();
3048 return $access || $is_instructor;
3049 }
3050
3051 /**
3052 * Filter user list args for instructor
3053 *
3054 * @since 3.0.0
3055 *
3056 * @param array $args args.
3057 *
3058 * @return array
3059 */
3060 public function user_list_args_for_instructor( $args ) {
3061 if ( User::is_instructor() ) {
3062 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3063 unset( $args['fields']['user_email'] );
3064 }
3065 }
3066
3067 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3068 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3069 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3070 }
3071
3072 return $args;
3073 }
3074 }
3075