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