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