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