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