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