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