PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.6
Tutor LMS – eLearning and online course solution v4.0.6
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 2 weeks ago Announcements.php 1 month ago Assets.php 1 month 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 2 weeks 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 2 weeks 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 1 month ago UserPreference.php 1 month ago Utils.php 2 weeks ago Video_Stream.php 2 weeks 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
3577 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 = tutor_utils()->filter_video_meta( $video );
1970 $video_source = tutor_utils()->array_get( 'source', $video );
1971 if ( -1 !== $video_source ) {
1972 update_post_meta( $post_ID, '_video', $video );
1973 } elseif ( ! tutor_is_rest() ) {
1974 delete_post_meta( $post_ID, '_video' );
1975 }
1976 }
1977
1978 /**
1979 * Adding author to instructor automatically
1980 */
1981 $requested_author_id = Input::post( 'post_author_override', 0, Input::TYPE_INT );
1982
1983 /**
1984 * Only accept the requested author override if it targets a real,approved instructor or admin
1985 *
1986 * @since 4.0.4
1987 */
1988 $author_id = $post->post_author;
1989 if ( $requested_author_id && ( User::is_admin() || User::is_instructor() ) ) {
1990 if ( User::is_admin( $requested_author_id ) || User::is_instructor( $requested_author_id, true ) ) {
1991 $author_id = $requested_author_id;
1992 }
1993 }
1994
1995 $attached = (int) $wpdb->get_var(
1996 $wpdb->prepare(
1997 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1998 WHERE user_id = %d
1999 AND meta_key = '_tutor_instructor_course_id'
2000 AND meta_value = %d ",
2001 $author_id,
2002 $post_ID
2003 )
2004 );
2005
2006 if ( ! $attached ) {
2007 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
2008 }
2009
2010 /**
2011 * Disable question and answer for this course
2012 *
2013 * @since 1.7.0
2014 */
2015 if ( $additional_data_edit ) {
2016 foreach ( $this->additional_meta as $key ) {
2017 //phpcs:ignore WordPress.Security.NonceVerification.Missing
2018 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
2019 }
2020 }
2021
2022 /**
2023 * Update course content if main author is changed and multi-instructor addon is disabled.
2024 *
2025 * @since 4.0.4
2026 */
2027 if ( ! $attached && ! tutor_utils()->is_addon_enabled( 'tutor-multi-instructors' ) ) {
2028 CourseModel::update_course_content_author( $post_ID, (int) $author_id );
2029 // Remove all existing instructors from the course and add the new one.
2030 delete_metadata( 'user', 0, '_tutor_instructor_course_id', $post_ID, true );
2031 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
2032 }
2033
2034 do_action( 'tutor_save_course_after', $post_ID, $post );
2035 }
2036
2037 /**
2038 * Save course topic
2039 *
2040 * @since 1.0.0
2041 * @since 3.0.0 response and input name updated.
2042 *
2043 * @return void
2044 */
2045 public function tutor_save_topic() {
2046 tutor_utils()->check_nonce();
2047
2048 $is_update = false;
2049 $errors = array();
2050 $topic_title = Input::post( 'title' );
2051
2052 if ( empty( $topic_title ) ) {
2053 $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
2054 $this->json_response(
2055 __( 'Invalid inputs', 'tutor' ),
2056 $errors,
2057 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
2058 );
2059 }
2060
2061 // Gather parameters.
2062 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2063 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2064 $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
2065
2066 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
2067
2068 // Validate if user can manage the topic.
2069 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
2070 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2071 }
2072
2073 // Create payload to create/update the topic.
2074 $post_arr = array(
2075 'post_type' => 'topics',
2076 'post_title' => $topic_title,
2077 'post_content' => $topic_summary,
2078 'post_status' => 'publish',
2079 'post_author' => get_current_user_id(),
2080 'post_parent' => $course_id,
2081 'menu_order' => $next_topic_order_id,
2082 );
2083
2084 if ( $topic_id ) {
2085 $is_update = true;
2086 $post_arr['ID'] = $topic_id;
2087 }
2088
2089 $current_topic_id = wp_insert_post( $post_arr );
2090
2091 if ( $is_update ) {
2092 $this->json_response(
2093 __( 'Topic updated successfully!', 'tutor' ),
2094 $current_topic_id
2095 );
2096 } else {
2097 $this->json_response(
2098 __( 'Topic created successfully!', 'tutor' ),
2099 $current_topic_id,
2100 HttpHelper::STATUS_CREATED
2101 );
2102 }
2103 }
2104
2105 /**
2106 * Delete a course topic
2107 *
2108 * @since 1.0.0
2109 * @since 3.0.0 code refactor and response updated.
2110 *
2111 * @return void
2112 */
2113 public function tutor_delete_topic() {
2114 tutor_utils()->check_nonce();
2115
2116 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2117 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
2118 $this->json_response(
2119 tutor_utils()->error_message(),
2120 null,
2121 HttpHelper::STATUS_FORBIDDEN
2122 );
2123 }
2124
2125 global $wpdb;
2126
2127 // Assign course ID to orphan content IDs since the topic will be deleted.
2128 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
2129 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
2130 foreach ( $content_ids as $content_id ) {
2131 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
2132 // Actually all kind of contents.
2133 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
2134 }
2135
2136 // Set contents under the topic orphan.
2137 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
2138
2139 // Then delete the topic from database.
2140 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
2141 wp_delete_post( $topic_id );
2142
2143 $this->json_response(
2144 __( 'Topic deleted successfully!', 'tutor' )
2145 );
2146 }
2147
2148 /**
2149 * Handle enroll now action
2150 *
2151 * @since 1.0.0
2152 *
2153 * @return void
2154 */
2155 public function enroll_now() {
2156
2157 if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
2158 return;
2159 }
2160
2161 // Checking Nonce.
2162 tutor_utils()->checking_nonce();
2163
2164 $user_id = get_current_user_id();
2165 if ( ! $user_id ) {
2166 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
2167 }
2168
2169 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
2170
2171 /**
2172 * TODO: need to check purchase information
2173 */
2174
2175 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2176
2177 $course = get_post( $course_id );
2178
2179 if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
2180 wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
2181 }
2182
2183 /**
2184 * If is is not purchasable, it's free, and enroll right now
2185 * If purchasable, then process purchase.
2186 *
2187 * @since: v.1.0.0
2188 */
2189 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2190 // Process purchase.
2191
2192 } else {
2193 // Free enroll.
2194 EnrollmentModel::do_enroll( $course_id );
2195 }
2196
2197 $referer_url = wp_get_referer();
2198 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2199 exit;
2200 }
2201
2202 /**
2203 * Mark complete completed
2204 *
2205 * @since 1.0.0
2206 *
2207 * @since 3.7.1 Filter hook: tutor_user_can_complete_course added
2208 *
2209 * @return void
2210 */
2211 public function mark_course_complete() {
2212 $tutor_action = Input::post( 'tutor_action' );
2213 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2214 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
2215 return;
2216 }
2217
2218 $permalink = get_the_permalink( $course_id );
2219
2220 // Checking nonce.
2221 tutor_utils()->checking_nonce();
2222
2223 $user_id = get_current_user_id();
2224
2225 // TODO: need to show view if not signed_in.
2226 if ( ! $user_id ) {
2227 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2228 }
2229
2230 if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2231 die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2232 }
2233
2234 /**
2235 * Filter hook provided to restrict course completion. This is useful
2236 * for specific cases like prerequisites. WP_Error should be returned
2237 * from the filter value to prevent the completion.
2238 */
2239 $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2240
2241 if ( is_wp_error( $can_complete ) ) {
2242 tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2243 } elseif ( ! $can_complete ) {
2244 tutor_utils()->redirect_to( $permalink, __( 'You do not have permission to complete this course.', 'tutor' ), 'error' );
2245 } else {
2246 CourseModel::mark_course_as_completed( $course_id, $user_id );
2247 // Set temporary identifier to show review pop up.
2248 self::set_review_popup_data( $user_id, $course_id );
2249
2250 wp_safe_redirect( $permalink );
2251 exit;
2252 }
2253 }
2254
2255 /**
2256 * Ajax course complete handler
2257 *
2258 * @since 4.0.0
2259 *
2260 * @return void
2261 */
2262 public function ajax_tutor_complete_course() {
2263 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2264
2265 // Checking nonce.
2266 if ( ! tutor_utils()->is_nonce_verified() ) {
2267 $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
2268 }
2269
2270 if ( ! $course_id ) {
2271 $this->response_bad_request( __( 'Invalid course', 'tutor' ) );
2272 }
2273
2274 $user_id = get_current_user_id();
2275 if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2276 $this->response_bad_request( __( 'You are not enrolled in this course', 'tutor' ) );
2277 }
2278
2279 /**
2280 * Filter hook provided to restrict course completion. This is useful
2281 * for specific cases like prerequisites. WP_Error should be returned
2282 * from the filter value to prevent the completion.
2283 */
2284 $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2285
2286 if ( is_wp_error( $can_complete ) ) {
2287 $this->response_bad_request( $can_complete->get_error_message() );
2288 } elseif ( ! $can_complete ) {
2289 $this->response_bad_request( __( 'You do not have permission to complete this course.', 'tutor' ) );
2290 } else {
2291 CourseModel::mark_course_as_completed( $course_id, $user_id );
2292 // Set temporary identifier to show review pop up.
2293 self::set_review_popup_data( $user_id, $course_id );
2294
2295 $this->response_success( __( 'Course completed successfully', 'tutor' ) );
2296 }
2297 }
2298
2299 /**
2300 * Set data for review popup.
2301 *
2302 * @since 2.2.5
2303 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2304 *
2305 * @param int $user_id user id.
2306 * @param int $course_id course id.
2307 *
2308 * @return void
2309 */
2310 public static function set_review_popup_data( $user_id, $course_id ) {
2311 if ( get_tutor_option( 'enable_course_review' ) ) {
2312 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2313 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2314 $meta_key = User::get_review_popup_meta( $course_id );
2315 add_user_meta( $user_id, $meta_key, $course_id, true );
2316 }
2317 }
2318 }
2319
2320 /**
2321 * Popup review form on course details
2322 *
2323 * @since 1.0.0
2324 * @return void
2325 */
2326 public function popup_review_form() {
2327 if ( ! is_user_logged_in() ) {
2328 return;
2329 }
2330
2331 $is_learning_area = tutor_utils()->is_learning_area();
2332 $is_legacy_learning = tutor_utils()->is_legacy_learning_mode();
2333 if ( $is_legacy_learning && $is_learning_area ) {
2334 return;
2335 }
2336
2337 $course_id = get_the_ID();
2338
2339 if ( $is_learning_area && ! Input::has( 'subpage' ) ) {
2340 $course_id = wp_get_post_parent_id( wp_get_post_parent_id( get_the_ID() ) );
2341 }
2342
2343 if ( empty( $course_id ) ) {
2344 return;
2345 }
2346
2347 $user_id = get_current_user_id();
2348 $meta_key = User::get_review_popup_meta( $course_id );
2349 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2350 if ( is_single() && $course_id === $review_course_id ) {
2351 include tutor()->path . 'views/modal/review.php';
2352 }
2353 }
2354
2355 /**
2356 * Review popup data clear
2357 *
2358 * @since 2.4.0
2359 *
2360 * @return void
2361 */
2362 public function clear_review_popup_data() {
2363 tutils()->checking_nonce();
2364
2365 if ( is_user_logged_in() ) {
2366 $user_id = get_current_user_id();
2367 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2368
2369 if ( $course_id ) {
2370 $meta_key = User::get_review_popup_meta( $course_id );
2371 delete_user_meta( $user_id, $meta_key, $course_id );
2372 }
2373
2374 wp_send_json_success();
2375 }
2376 }
2377
2378 /**
2379 * Delete course delete from frontend dashboard
2380 *
2381 * @since 2.0.0
2382 *
2383 * @return void
2384 */
2385 public function tutor_delete_dashboard_course() {
2386 tutor_utils()->checking_nonce();
2387
2388 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2389 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2390 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2391 }
2392
2393 /**
2394 * Co-instructor can not delete a course
2395 *
2396 * @since 2.1.6
2397 */
2398 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2399 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2400 }
2401
2402 // Check if user is only an instructor.
2403 if ( ! User::is_admin() ) {
2404 // Check if instructor can trash course.
2405 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2406
2407 if ( ! $can_trash_post ) {
2408 wp_send_json_error( tutor_utils()->error_message() );
2409 }
2410 }
2411
2412 $trash_course = wp_update_post(
2413 array(
2414 'ID' => $course_id,
2415 'post_status' => CourseModel::STATUS_TRASH,
2416 )
2417 );
2418
2419 if ( $trash_course ) {
2420 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2421 }
2422 wp_send_json_success();
2423 }
2424
2425 /**
2426 * Main author change from gutenberg editor
2427 *
2428 * @since 2.0.0
2429 *
2430 * @param array $data data.
2431 * @param array $postarr post array.
2432 *
2433 * @return mixed
2434 */
2435 public function tutor_add_gutenberg_author( $data, $postarr ) {
2436 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2437 $post_type = $postarr['post_type'];
2438 $courses_post_type = tutor()->course_post_type;
2439
2440 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2441 return $data;
2442 }
2443
2444 /**
2445 * Only admin can change main author
2446 */
2447 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2448 global $wpdb;
2449 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2450 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2451
2452 if ( $post_author > 0 ) {
2453 $data['post_author'] = $post_author;
2454 } else {
2455 $data['post_author'] = get_current_user_id();
2456 }
2457 }
2458
2459 return $data;
2460 }
2461
2462
2463 /**
2464 * Attach product with course when course save from frontend or backend.
2465 *
2466 * @since 1.3.4
2467 *
2468 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2469 *
2470 * @param integer $post_ID course ID.
2471 * @param array $post_data created course post details.
2472 *
2473 * @return void
2474 */
2475 public function attach_product_with_course( $post_ID, $post_data ) {
2476 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2477 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2478
2479 /**
2480 * For native monetization, just return
2481 * No need to attach anything.
2482 */
2483 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2484 return;
2485 }
2486
2487 /**
2488 * When course moved paid to free
2489 * Keep the product linked and return.
2490 */
2491 if ( -1 === $product_id ) {
2492 return;
2493 }
2494
2495 /**
2496 * Free user can only select product from dropdown
2497 */
2498 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2499 if ( $product_id > 0 ) {
2500 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2501 }
2502
2503 return;
2504 }
2505
2506 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2507 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2508 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2509
2510 if ( ! $course_price || $sale_price >= $course_price ) {
2511 return;
2512 }
2513
2514 $course = get_post( $post_ID );
2515
2516 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2517
2518 if ( 'wc' === $monetize_by ) {
2519
2520 $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2521
2522 if ( $is_update ) {
2523 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2524
2525 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2526 $product_obj = wc_get_product( $product_id );
2527 if ( $product_obj->is_type( 'subscription' ) ) {
2528 update_post_meta( $product_id, '_subscription_price', $course_price );
2529 }
2530
2531 // Set course regular & sale price.
2532 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2533 } else {
2534 // Create new WC product name with course title.
2535 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2536 if ( $product_id ) {
2537 $product_obj = wc_get_product( $product_id );
2538
2539 self::sync_course_with_wc_product( $post_ID, $product_id );
2540
2541 // Set course regular & sale price.
2542 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2543 }
2544 }
2545
2546 $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2547 if ( $product_id && $course_post_thumbnail ) {
2548 set_post_thumbnail( $product_id, $course_post_thumbnail );
2549 }
2550 } elseif ( 'edd' === $monetize_by ) {
2551
2552 $is_update = false;
2553
2554 if ( $attached_product_id ) {
2555 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2556 if ( $edd_price ) {
2557 $is_update = true;
2558 }
2559 }
2560
2561 if ( $is_update ) {
2562 // Update the product.
2563 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2564 } else {
2565 // Create new product.
2566
2567 $post_arr = array(
2568 'post_type' => 'download',
2569 'post_title' => $course->post_title,
2570 'post_status' => 'publish',
2571 'post_author' => get_current_user_id(),
2572 );
2573 $download_id = wp_insert_post( $post_arr );
2574 if ( $download_id ) {
2575 // EDD edd_price.
2576 update_post_meta( $download_id, 'edd_price', $course_price );
2577
2578 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2579 // Mark product for EDD.
2580 update_post_meta( $download_id, '_tutor_product', 'yes' );
2581
2582 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2583 if ( $course_post_thumbnail ) {
2584 set_post_thumbnail( $download_id, $course_post_thumbnail );
2585 }
2586 }
2587 }
2588 }
2589 }
2590
2591 /**
2592 * Check if course starting
2593 *
2594 * @since 1.4.8
2595 * @return void
2596 */
2597 public function tutor_lesson_load_before() {
2598 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2599 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2600 if ( is_user_logged_in() ) {
2601 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2602 if ( ! $completed_lessons && ! $is_course_started ) {
2603 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2604 do_action( 'tutor/course/started', $course_id );
2605 }
2606 }
2607 }
2608
2609 /**
2610 * Add Course level to course settings
2611 *
2612 * @since 1.4.8
2613 *
2614 * @return void
2615 */
2616 public function course_elements_enable_disable() {
2617 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2618 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2619 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2620 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2621 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2622 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2623 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2624 }
2625
2626 /**
2627 * Enable disable course progress bar
2628 *
2629 * @since 1.4.8
2630 *
2631 * @param string $html HTML string.
2632 * @return string
2633 */
2634 public function enable_disable_course_progress_bar( $html ) {
2635 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2636 if ( $disable_option ) {
2637 return '';
2638 }
2639 return $html;
2640 }
2641
2642 /**
2643 * Enable disable material includes
2644 *
2645 * @since 1.4.8
2646 *
2647 * @param string $html HTML string.
2648 *
2649 * @return string
2650 */
2651 public function enable_disable_material_includes( $html ) {
2652 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true );
2653 if ( $disable_option ) {
2654 return '';
2655 }
2656 return $html;
2657 }
2658
2659 /**
2660 * Enable disable course content
2661 *
2662 * @since 1.4.8
2663 *
2664 * @param string $html HTML string.
2665 *
2666 * @return string
2667 */
2668 public function enable_disable_course_content( $html ) {
2669 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2670 if ( $disable_option ) {
2671 return '';
2672 }
2673 return $html;
2674 }
2675
2676 /**
2677 * Enable disable course benefits
2678 *
2679 * @since 1.4.8
2680 *
2681 * @param string $html HTML string.
2682 *
2683 * @return string
2684 */
2685 public function enable_disable_course_benefits( $html ) {
2686 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2687 if ( $disable_option ) {
2688 return '';
2689 }
2690 return $html;
2691 }
2692
2693 /**
2694 * Enable disable course requirements
2695 *
2696 * @since 1.4.8
2697 *
2698 * @param string $html HTML string.
2699 *
2700 * @return string
2701 */
2702 public function enable_disable_course_requirements( $html ) {
2703 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2704 if ( $disable_option ) {
2705 return '';
2706 }
2707 return $html;
2708 }
2709
2710 /**
2711 * Enable disable course target audience
2712 *
2713 * @since 1.4.8
2714 *
2715 * @param string $html HTML string.
2716 *
2717 * @return string
2718 */
2719 public function enable_disable_course_target_audience( $html ) {
2720 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2721 if ( $disable_option ) {
2722 return '';
2723 }
2724 return $html;
2725 }
2726
2727 /**
2728 * Enable disable course nav items
2729 *
2730 * @since 1.4.8
2731 *
2732 * @param array $items item list.
2733 * @param integer $course_id course ID.
2734 *
2735 * @return array
2736 */
2737 public function enable_disable_course_nav_items( $items, $course_id ) {
2738 global $wp_query, $post;
2739 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2740 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2741 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2742
2743 // Whether Q&A enabled.
2744 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2745 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2746 unset( $items['questions'] );
2747 }
2748 }
2749
2750 // Whether announcment enabled.
2751 if ( $disable_course_announcements ) {
2752 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2753 unset( $items['announcements'] );
2754 }
2755 }
2756
2757 // Hide review section if disabled.
2758 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2759 unset( $items['reviews'] );
2760 }
2761
2762 // Whether enrollment require.
2763 $is_enrolled = EnrollmentModel::is_enrolled();
2764
2765 return array_filter(
2766 $items,
2767 function ( $item ) use ( $is_enrolled ) {
2768 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2769 return $is_enrolled;
2770 }
2771 return true;
2772 }
2773 );
2774 }
2775
2776 /**
2777 * Filter product in shop page
2778 *
2779 * @since 1.4.9
2780 *
2781 * @return void|null
2782 */
2783 public function filter_product_in_shop_page() {
2784 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2785 if ( ! $hide_course_from_shop_page ) {
2786 return;
2787 }
2788 add_filter( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2789 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10 );
2790 add_filter( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2791 }
2792
2793
2794 /**
2795 * Tutor product meta query
2796 *
2797 * @since 1.4.9
2798 *
2799 * @return array
2800 */
2801 public function tutor_product_meta_query() {
2802 $meta_query = array(
2803 'key' => '_tutor_product',
2804 'compare' => 'NOT EXISTS',
2805 );
2806 return $meta_query;
2807 }
2808
2809 /**
2810 * Filter product in woocommerce shop page
2811 *
2812 * @since 1.4.9
2813 *
2814 * @param \WP_Query $wp_query WP Query instance.
2815 *
2816 * @return \WP_Query
2817 */
2818 public function filter_woocommerce_product_query( $wp_query ) {
2819 $product_ids = $this->get_connected_wc_product_ids();
2820 $wp_query->set( 'post__not_in', $product_ids );
2821 return $wp_query;
2822 }
2823
2824 /**
2825 * Get connected woocommerce product ids for course and course bundle
2826 *
2827 * @since 2.7.2
2828 *
2829 * @return array
2830 */
2831 public function get_connected_wc_product_ids() {
2832 global $wpdb;
2833
2834 $results = $wpdb->get_results(
2835 $wpdb->prepare(
2836 "SELECT DISTINCT pm.meta_value product_id
2837 FROM {$wpdb->posts} p
2838 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2839 AND pm.meta_key = %s
2840 WHERE post_type IN( 'courses','course-bundle' )",
2841 '_tutor_course_product_id'
2842 )
2843 );
2844
2845 $ids = array();
2846 if ( is_array( $results ) && count( $results ) ) {
2847 $ids = array_column( $results, 'product_id' );
2848 }
2849
2850 return $ids;
2851 }
2852
2853 /**
2854 * Filter product in edd downloads shortcode page
2855 *
2856 * @since 1.4.9
2857 *
2858 * @param \WP_Query $query WP Query instance.
2859 *
2860 * @return \WP_Query
2861 */
2862 public function filter_edd_downloads_query( $query ) {
2863 $query['meta_query'][] = $this->tutor_product_meta_query();
2864 return $query;
2865 }
2866
2867 /**
2868 * Filter product in edd downloads archive page
2869 *
2870 * @since 1.4.9
2871 *
2872 * @param \WP_Query $wp_query WP Query instance.
2873 *
2874 * @return \WP_Query
2875 */
2876 public function filter_archive_meta_query( $wp_query ) {
2877 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2878 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2879 }
2880 return $wp_query;
2881 }
2882
2883 /**
2884 * Removed course price if already enrolled at single course
2885 *
2886 * @since 1.5.8
2887 *
2888 * @param string $html HTML string.
2889 *
2890 * @return string
2891 */
2892 public function remove_price_if_enrolled( $html ) {
2893 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2894
2895 if ( $should_removed ) {
2896 $course_id = get_the_ID();
2897 $enrolled = EnrollmentModel::is_enrolled( $course_id );
2898 if ( $enrolled ) {
2899 $html = '';
2900 }
2901 }
2902 return $html;
2903 }
2904
2905 /**
2906 * Get course completion missing requirements message.
2907 *
2908 * @since 4.0.0
2909 *
2910 * @param int $course_id Course ID.
2911 * @param int $user_id User ID.
2912 *
2913 * @return string|null
2914 */
2915 public static function get_course_completion_restrict_msg( $course_id = 0, $user_id = 0 ) {
2916 $course_id = tutor_utils()->get_post_id( $course_id );
2917 $user_id = tutor_utils()->get_user_id( $user_id );
2918
2919 if ( 'strict' !== tutor_utils()->get_option( 'course_completion_process' ) ) {
2920 return null;
2921 }
2922
2923 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
2924 $total_lessons = tutor_utils()->get_lesson_count_by_course( $course_id ); // @phpstan-ignore method.notFound
2925
2926 if ( $completed_lessons < $total_lessons ) {
2927 return __( 'Complete all lessons to mark this course as complete', 'tutor' );
2928 }
2929
2930 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
2931
2932 $required_quiz_pass_count = 0;
2933 $required_assignment_pass_count = 0;
2934
2935 $is_assignment_addon_enabled = tutor_utils()->is_addon_enabled( 'tutor-assignments' );
2936
2937 foreach ( $course_contents as $content ) {
2938
2939 if ( tutor()->quiz_post_type === $content->post_type ) {
2940 if ( ! QuizModel::is_quiz_passed( $content->ID, $user_id ) ) {
2941 ++$required_quiz_pass_count;
2942 }
2943 }
2944
2945 if ( $is_assignment_addon_enabled && tutor()->assignment_post_type === $content->post_type ) {
2946 if ( ! Assignments::is_assignment_passed( $content->ID, $user_id ) ) {
2947 ++$required_assignment_pass_count;
2948 }
2949 }
2950 }
2951
2952 if ( ! $required_quiz_pass_count && ! $required_assignment_pass_count ) {
2953 return null;
2954 }
2955
2956 return self::get_course_completion_requirement_message(
2957 $required_quiz_pass_count,
2958 $required_assignment_pass_count
2959 );
2960 }
2961
2962 /**
2963 * Build missing course completion requirements message.
2964 *
2965 * @since 4.0.0
2966 *
2967 * @param int $quiz_count Total quiz count.
2968 * @param int $assignment_count Total assignment count.
2969 *
2970 * @return string
2971 */
2972 private static function get_course_completion_requirement_message( $quiz_count, $assignment_count ) {
2973 $quiz_label = _n( 'quiz', 'quizzes', $quiz_count, 'tutor' );
2974 $assignment_label = _n( 'assignment', 'assignments', $assignment_count, 'tutor' );
2975
2976 if ( $quiz_count && ! $assignment_count ) {
2977 return sprintf(
2978 /* translators: %1$s: item count; %2$s: item label */
2979 __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2980 $quiz_count,
2981 $quiz_label
2982 );
2983 }
2984
2985 if ( ! $quiz_count && $assignment_count ) {
2986 return sprintf(
2987 /* translators: %1$s: item count; %2$s: item label */
2988 __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2989 $assignment_count,
2990 $assignment_label
2991 );
2992 }
2993
2994 return sprintf(
2995 /* translators: %1$s: quiz count; %2$s: quiz label; %3$s: assignment count; %4$s: assignment label */
2996 __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ),
2997 $quiz_count,
2998 $quiz_label,
2999 $assignment_count,
3000 $assignment_label
3001 );
3002 }
3003
3004 /**
3005 * Check if all lessons and quizzes done before mark course complete.
3006 *
3007 * @since 1.5.8
3008 *
3009 * @param string $html HTML string.
3010 *
3011 * @return string
3012 */
3013 public function tutor_lms_hide_course_complete_btn( $html ) {
3014 $_msg = self::get_course_completion_restrict_msg();
3015
3016 if ( $_msg ) {
3017 return '<div class="tutor-alert tutor-warning tutor-mt-28">
3018 <div class="tutor-alert-text">
3019 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
3020 <span>' . $_msg . '</span>
3021 </div>
3022 </div>';
3023 }
3024
3025 return $html;
3026 }
3027
3028 /**
3029 * Generate Gradebook
3030 *
3031 * @since 1.5.8
3032 *
3033 * @param string $html HTML string.
3034 *
3035 * @return string
3036 */
3037 public function get_generate_greadbook( $html ) {
3038 if ( ! tutor_utils()->is_completed_course() ) {
3039 return '';
3040 }
3041 return $html;
3042 }
3043
3044 /**
3045 * Add social share content in header
3046 *
3047 * @since 1.6.3
3048 *
3049 * @return void
3050 */
3051 public function social_share_content() {
3052 global $wp_query, $post;
3053 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
3054 <!--Facebook-->
3055 <meta property="og:type" content="website"/>
3056 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
3057 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
3058 <!--Twitter-->
3059 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
3060 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
3061 <!--Google+-->
3062 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
3063 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
3064 <?php
3065 }
3066 }
3067
3068 /**
3069 * Delete associated enrollment
3070 *
3071 * @since 1.8.2
3072 *
3073 * @param integer $post_id post ID.
3074 *
3075 * @return void
3076 */
3077 public function delete_associated_enrollment( $post_id ) {
3078 global $wpdb;
3079
3080 $enroll_id = $wpdb->get_var(
3081 $wpdb->prepare(
3082 "SELECT
3083 post_id
3084 FROM
3085 {$wpdb->postmeta}
3086 WHERE
3087 meta_key=%s
3088 AND meta_value = %d
3089 ",
3090 EnrollmentModel::ENROLLMENT_ORDER_ID_META,
3091 $post_id
3092 )
3093 );
3094
3095 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
3096
3097 $course_id = get_post_field( 'post_parent', $enroll_id );
3098 $user_id = get_post_field( 'post_author', $enroll_id );
3099
3100 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
3101 }
3102 }
3103
3104 /**
3105 * Reset course progress.
3106 *
3107 * @since 1.5.8
3108 *
3109 * @return void
3110 */
3111 public function tutor_reset_course_progress() {
3112 tutor_utils()->checking_nonce();
3113 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3114 $context = Input::post( 'context', '' );
3115 $course_reset_progress = tutor_utils()->get_option( 'course_reset_progress', false );
3116 $course_retake_feature = tutor_utils()->get_option( 'course_retake_feature', false );
3117
3118 if ( ! $course_reset_progress && 'learning-area-sidebar' === $context ) {
3119 $this->response_bad_request( __( 'You do not have permission to reset this course.', 'tutor' ) );
3120 return;
3121 }
3122
3123 if ( ! $course_retake_feature && ( 'course-landing' === $context || 'learning-area' === $context ) ) {
3124 $this->response_bad_request( __( 'You do not have permission to retake this course.', 'tutor' ) );
3125 return;
3126 }
3127
3128 if ( ! $course_id || ! is_numeric( $course_id ) || ! EnrollmentModel::is_enrolled( $course_id ) ) {
3129 $this->response_bad_request( __( 'Invalid Course ID or Access Denied.', 'tutor' ) );
3130 return;
3131 }
3132
3133 tutor_utils()->delete_course_progress( $course_id );
3134 $this->json_response( '', array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
3135 }
3136
3137 /**
3138 * Do enroll if guest attempt to enroll and course is free
3139 *
3140 * @since 1.9.8
3141 *
3142 * @param integer $course_id course ID.
3143 * @param integer $user_id user ID.
3144 *
3145 * @return void
3146 */
3147 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
3148 $course_id = sanitize_text_field( $course_id );
3149 $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
3150
3151 if ( $course_id && $is_allowed ) {
3152 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
3153 if ( ! $is_purchasable ) {
3154 EnrollmentModel::do_enroll( $course_id, $order_id = 0, $user_id );
3155 do_action( 'guest_attempt_after_enrollment', $course_id );
3156 }
3157 }
3158 }
3159
3160 /**
3161 * Handle course enrollment
3162 *
3163 * @since 2.1.0
3164 *
3165 * @return void
3166 */
3167 public function course_enrollment() {
3168 tutor_utils()->checking_nonce();
3169
3170 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3171 $user_id = get_current_user_id();
3172
3173 if ( ! $course_id || ! $user_id ) {
3174 wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
3175 }
3176
3177 $password_protected = post_password_required( $course_id );
3178 if ( $password_protected ) {
3179 wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3180 }
3181
3182 $course = get_post( $course_id );
3183
3184 if ( CourseModel::STATUS_PRIVATE === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3185 wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3186 }
3187
3188 /**
3189 * This check was added to address a security issue where users could
3190 * enroll in a course via an AJAX call without purchasing it.
3191 *
3192 * To prevent this, we now verify whether the course is paid.
3193 * Additionally, we check if the user is already enrolled, since
3194 * Tutor's default behavior enrolls users automatically upon purchase.
3195 *
3196 * @since 3.9.4
3197 */
3198 if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3199 $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id );
3200
3201 if ( ! $is_enrolled ) {
3202 wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3203 }
3204 }
3205
3206 $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id );
3207 if ( $enroll ) {
3208 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3209 } else {
3210 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3211 }
3212 }
3213
3214 /**
3215 * After trash a course direct to the course list page
3216 *
3217 * @since 2.1.7
3218 *
3219 * @param integer $post_id int course id.
3220 *
3221 * @return void
3222 */
3223 public static function redirect_to_course_list_page( int $post_id ): void {
3224 $post = get_post( $post_id );
3225 if ( tutor()->course_post_type === $post->post_type ) {
3226 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
3227 if ( ! $is_gutenberg_enabled ) {
3228 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
3229 exit;
3230 }
3231 }
3232 }
3233
3234 /**
3235 * Create or update WooCommerce product
3236 *
3237 * If product id not set it will create new one.
3238 *
3239 * @since 2.2.0
3240 *
3241 * @param string $title product title.
3242 * @param string $reg_price product price.
3243 * @param string $sale_price product sale price.
3244 * @param int $product_id product ID.
3245 * @param string $status product status.
3246 *
3247 * @return integer Product id or return 0 if WC not exists
3248 */
3249 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
3250 if ( ! tutor_utils()->has_wc() ) {
3251 return 0;
3252 }
3253
3254 $product_obj = new \WC_Product();
3255 if ( $product_id ) {
3256 $product_obj = wc_get_product( $product_id );
3257 }
3258
3259 $product_obj->set_name( $title );
3260 $product_obj->set_status( $status );
3261 $product_obj->set_price( $reg_price );
3262 $product_obj->set_regular_price( $reg_price );
3263
3264 if ( $sale_price > 0 ) {
3265 $product_obj->set_sale_price( $sale_price );
3266 } else {
3267 $product_obj->set_sale_price( null );
3268 }
3269
3270 $product_obj->set_sold_individually( true );
3271
3272 return $product_obj->save();
3273 }
3274
3275 /**
3276 * Get course/bundle mini info
3277 *
3278 * @since 3.0.0
3279 *
3280 * @param object $post Course or bundle post.
3281 *
3282 * @return array
3283 */
3284 public static function get_mini_info( object $post ) {
3285 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3286 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3287 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3288 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
3289
3290 $info = array(
3291 'id' => $post->ID,
3292 'title' => $post->post_title,
3293 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3294 'is_purchasable' => $is_purchasable,
3295 'regular_price' => $regular_price,
3296 'sale_price' => $sale_price,
3297 );
3298
3299 $card_data = apply_filters( 'tutor_course_mini_info', $info, $post );
3300
3301 return $card_data;
3302 }
3303
3304 /**
3305 * Get course/bundle card data
3306 *
3307 * This method will return all data that contain in
3308 * course card
3309 *
3310 * @since 3.0.0
3311 *
3312 * @param object $post Course or bundle post.
3313 *
3314 * @return array
3315 */
3316 public static function get_card_data( object $post ) {
3317 $info = self::get_mini_info( $post );
3318
3319 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3320 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3321 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3322
3323 $card_data = apply_filters( 'tutor_course_card_data', $info, $post );
3324
3325 return $card_data;
3326 }
3327
3328 /**
3329 * Filter user list access for instructor
3330 *
3331 * @since 3.0.0
3332 *
3333 * @param bool $access access.
3334 *
3335 * @return bool
3336 */
3337 public function user_list_access_for_instructor( $access ) {
3338 $is_instructor = User::is_instructor();
3339 return $access || $is_instructor;
3340 }
3341
3342 /**
3343 * Filter user list args for instructor
3344 *
3345 * @since 3.0.0
3346 *
3347 * @param array $args args.
3348 *
3349 * @return array
3350 */
3351 public function user_list_args_for_instructor( $args ) {
3352 if ( User::is_instructor() ) {
3353 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3354 unset( $args['fields']['user_email'] );
3355 }
3356 }
3357
3358 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3359 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3360 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3361 }
3362
3363 return $args;
3364 }
3365
3366 /**
3367 * Get a list of possible course status.
3368 *
3369 * @since 3.6.2
3370 *
3371 * @return array
3372 */
3373 public static function course_status_list() {
3374 return array(
3375 'publish',
3376 'private',
3377 'draft',
3378 'trash',
3379 'pending',
3380 'future',
3381 );
3382 }
3383
3384 /**
3385 * Link a course/bundle post to a WooCommerce product.
3386 *
3387 * @since 3.8.2
3388 *
3389 * @param int $post_ID The WordPress post ID of the course.
3390 * @param int $product_id The WooCommerce product ID to associate with the course.
3391 *
3392 * @return void
3393 */
3394 public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3395
3396 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
3397
3398 // Mark product for woocommerce.
3399 update_post_meta( $product_id, '_virtual', 'yes' );
3400 update_post_meta( $product_id, '_tutor_product', 'yes' );
3401 }
3402
3403 /**
3404 * Map Tutor's course prices to WooCommerce.
3405 *
3406 * @since 3.8.2
3407 *
3408 * @param int $post_ID The WordPress post ID of the course.
3409 * @param string|int|float $regular_price The regular price.
3410 * @param string|int|float $sale_price The sale price.
3411 * @return void
3412 */
3413 private static function set_course_regular_and_sale_price( $post_ID, $regular_price, $sale_price ) {
3414
3415 // Set course regular & sale price.
3416 update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3417 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3418 }
3419
3420 /**
3421 * Render start/resume button in enrolled course action btn.
3422 *
3423 * @since 4.0.0
3424 *
3425 * @param int $course_id course id.
3426 *
3427 * @return void
3428 */
3429 public function render_course_action_btn( int $course_id ) {
3430 $is_completed = tutor_utils()->is_completed_course( $course_id );
3431 if ( $is_completed ) {
3432 $certificate_addon_active = tutor_utils()->is_addon_enabled( 'tutor-certificate' );
3433 if ( ! $certificate_addon_active ) {
3434 Button::make()
3435 ->tag( 'a' )
3436 ->label( __( 'Completed', 'tutor' ) )
3437 ->icon( Icon::COMPLETED_CIRCLE )
3438 ->variant( Variant::PRIMARY )
3439 ->size( Size::X_SMALL )
3440 ->attr( 'href', esc_url( get_the_permalink( $course_id ) ) )
3441 ->render();
3442 }
3443 return;
3444 }
3445
3446 $course_progress = tutor_utils()->get_course_completed_percent( $course_id, 0, true );
3447 $button_text = $course_progress['completed_percent'] > 0 ? __( 'Resume', 'tutor' ) : __( 'Start', 'tutor' );
3448 $button_url = tutor_utils()->get_course_first_lesson( $course_id );
3449
3450 if ( ! $button_url ) {
3451 $button_url = get_the_permalink( $course_id );
3452 }
3453
3454 Button::make()
3455 ->tag( 'a' )
3456 ->label( $button_text )
3457 ->icon( Icon::PLAY_2 )
3458 ->variant( Variant::PRIMARY )
3459 ->size( Size::X_SMALL )
3460 ->attr( 'href', esc_url( $button_url ) )
3461 ->render();
3462 }
3463
3464 /**
3465 * Get the content for the course completion modal.
3466 *
3467 * This method returns HTML for displaying a modal that informs the user
3468 * that they have not completed all required lessons and assessments. It includes
3469 * the user's current progress shown as a percentage and a progress bar.
3470 *
3471 * @since 4.0.0
3472 *
3473 * @param float $course_progress The completion percentage of the course.
3474 *
3475 * @return string The rendered HTML content for the modal.
3476 */
3477 public static function get_complete_modal_content( float $course_progress = 0 ): string {
3478 ob_start();
3479 ?>
3480 <div>
3481 <p class="tutor-p3 tutor-text-secondary tutor-text-center tutor-mb-7 tutor-px-11">
3482 <?php esc_html_e( 'You have not completed all required lessons and assessments. ', 'tutor' ); ?>
3483 </p>
3484 <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">
3485 <div class="tutor-flex tutor-items-center tutor-justify-between">
3486 <div class="tutor-p3 tutor-text-secondary">
3487 <?php esc_html_e( 'Your Progress', 'tutor' ); ?>
3488 </div>
3489 <div class="tutor-p1 tutor-font-bold">
3490 <?php echo esc_html( number_format_i18n( $course_progress ) . '%' ); ?>
3491 </div>
3492 </div>
3493 <?php
3494 Progress::make()->type( 'bar' )->value( $course_progress )->render();
3495 ?>
3496 </div>
3497 </div>
3498 <?php
3499 return ob_get_clean();
3500 }
3501
3502 /**
3503 * Render the course complete button.
3504 *
3505 * Displays a button that allows the user to complete the course. If the course
3506 * progress is less than 100%, clicking the button shows a modal informing the user
3507 * that not all requirements are fulfilled. If the course progress is 100%,
3508 * clicking the button attempts to complete the course.
3509 *
3510 * @since 4.0.0
3511 *
3512 * @param string $modal_id The HTML ID of the modal to display if not complete.
3513 * @param int $course_id The ID of the course.
3514 * @param float $course_progress The current completion percentage of the course.
3515 * @param string $size The button size.
3516 * @param string $tooltip Optional. Tooltip message.
3517 * @param bool $block Optional. Whether the button is full-width.
3518 *
3519 * @return void
3520 */
3521 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 {
3522 $button = Button::make()
3523 ->variant( Variant::SECONDARY )
3524 ->label( __( 'Complete the Course', 'tutor' ) )
3525 ->icon( Icon::TICK_MARK )
3526 ->size( $size )
3527 ->block( $block )
3528 ->attr( 'type', 'button' );
3529
3530 if ( ! empty( $tooltip ) ) {
3531 $button->disabled();
3532 }
3533
3534 if ( $course_progress < 100 ) {
3535 $button->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" );
3536 } else {
3537 $button->attr( '@click', "handleCourseComplete({$course_id})" );
3538 $button->attr( ':class', "courseCompleteMutation?.isPending ? 'tutor-btn-loading' : ''" );
3539 $button->attr( ':disabled', 'courseCompleteMutation?.isPending' );
3540 }
3541
3542 if ( ! empty( $tooltip ) ) {
3543 Tooltip::make()
3544 ->content( $tooltip )
3545 ->placement( Tooltip::PLACEMENT_BOTTOM )
3546 ->arrow( Tooltip::ARROW_CENTER )
3547 ->trigger_element( $button->get() )
3548 ->render();
3549 } else {
3550 $button->render();
3551 }
3552 }
3553
3554 /**
3555 * Render course retake button
3556 *
3557 * @since 4.0.0
3558 *
3559 * @param string $modal_id Modal id.
3560 * @param string $size The button size.
3561 * @param bool $block Optional. Whether the button is full-width.
3562 *
3563 * @return void
3564 */
3565 public static function render_course_retake_btn( string $modal_id, string $size = Size::MEDIUM, bool $block = false ): void {
3566 Button::make()
3567 ->variant( Variant::SECONDARY )
3568 ->label( __( 'Retake this Course', 'tutor' ) )
3569 ->icon( Icon::RELOAD_4 )
3570 ->size( $size )
3571 ->block( $block )
3572 ->attr( 'type', 'button' )
3573 ->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" )
3574 ->render();
3575 }
3576 }
3577