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