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