PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.4
Tutor LMS – eLearning and online course solution v3.9.4
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 6 months ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 10 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 6 months ago QuizBuilder.php 11 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 7 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 1 year ago Utils.php 7 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
3235 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, 'tutor_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 }
1178
1179 $this->json_response(
1180 __( 'Builder unlinked successfully.', 'tutor' ),
1181 $course_id,
1182 HttpHelper::STATUS_OK
1183 );
1184 }
1185
1186 /**
1187 * Get all course contents by course id.
1188 *
1189 * @since 3.0.0
1190 *
1191 * @param int $course_id course id.
1192 *
1193 * @return array
1194 */
1195 public function get_course_contents( $course_id ) {
1196 $data = array();
1197 $topics = tutor_utils()->get_topics( $course_id );
1198
1199 if ( $topics->have_posts() ) {
1200 foreach ( $topics->get_posts() as $post ) {
1201 $current_topic = array(
1202 'id' => $post->ID,
1203 'title' => $post->post_title,
1204 'summary' => $post->post_content,
1205 'contents' => array(),
1206 );
1207
1208 $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 );
1209
1210 if ( $topic_contents->have_posts() ) {
1211 foreach ( $topic_contents->get_posts() as $post ) {
1212 if ( tutor()->quiz_post_type === $post->post_type ) {
1213 $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1214 $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
1215 }
1216
1217 array_push( $current_topic['contents'], $post );
1218 }
1219 }
1220
1221 $current_topic = apply_filters( 'tutor_filter_course_content', $current_topic );
1222
1223 array_push( $data, $current_topic );
1224 }
1225 }
1226
1227 return $data;
1228 }
1229
1230 /**
1231 * Get course contents
1232 *
1233 * @since 3.0.0
1234 */
1235 public function ajax_course_contents() {
1236 tutor_utils()->check_nonce();
1237
1238 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1239
1240 $this->check_access( $course_id );
1241
1242 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1243 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1244 }
1245
1246 if ( ! empty( $errors ) ) {
1247 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1248 }
1249
1250 $contents = $this->get_course_contents( $course_id );
1251
1252 $this->json_response(
1253 __( 'Course contents fetched successfully', 'tutor' ),
1254 $contents
1255 );
1256 }
1257
1258 /**
1259 * Get course details by ID
1260 *
1261 * @since 3.0.0
1262 *
1263 * @return void
1264 */
1265 public function ajax_course_details() {
1266 tutor_utils()->check_nonce();
1267
1268 $errors = array();
1269 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1270
1271 $this->check_access( $course_id );
1272
1273 if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1274 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1275 }
1276
1277 if ( ! empty( $errors ) ) {
1278 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1279 }
1280
1281 $price_type = tutor_utils()->price_type( $course_id );
1282 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1283
1284 $product_name = '';
1285 $price = 0;
1286 $sale_price = 0;
1287 $product_id = tutor_utils()->get_course_product_id( $course_id );
1288
1289 if ( 'wc' === $monetize_by ) {
1290 $product = wc_get_product( $product_id );
1291 if ( $product ) {
1292 $product_name = $product->get_name();
1293 $price = $product->get_regular_price();
1294 $sale_price = $product->get_sale_price();
1295 }
1296 }
1297
1298 if ( 'tutor' === $monetize_by ) {
1299 $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1300 $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1301 }
1302
1303 $course_pricing = array(
1304 'type' => $price_type,
1305 'product_id' => $product_id,
1306 'product_name' => $product_name,
1307 'price' => $price,
1308 'sale_price' => $sale_price,
1309 );
1310
1311 $video_intro = get_post_meta( $course_id, '_video', true );
1312 if ( $video_intro ) {
1313 $source = $video_intro['source'] ?? '';
1314 if ( 'html5' === $source ) {
1315 $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1316 $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1317 $video['poster_url'] = $poster_url;
1318 $video['source_html5'] = $source_html5;
1319 }
1320 }
1321
1322 $course = get_post( $course_id, ARRAY_A );
1323 if ( $course ) {
1324 $course['post_name'] = urldecode( $course['post_name'] );
1325 }
1326
1327 $editors = tutor_utils()->get_editor_list( $course_id );
1328
1329 $data = array(
1330 'editors' => array_values( $editors ),
1331 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1332 'preview_link' => get_preview_post_link( $course_id ),
1333 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1334 'course_categories' => wp_get_post_terms( $course_id, CourseModel::COURSE_CATEGORY ),
1335 'course_tags' => wp_get_post_terms( $course_id, CourseModel::COURSE_TAG ),
1336 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1337 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
1338
1339 'enable_qna' => get_post_meta( $course_id, '_tutor_enable_qa', true ),
1340 'is_public_course' => get_post_meta( $course_id, '_tutor_is_public_course', true ),
1341 'course_level' => get_post_meta( $course_id, '_tutor_course_level', true ),
1342 'video' => $video_intro,
1343 'course_duration' => get_post_meta( $course_id, '_course_duration', true ),
1344 'course_benefits' => get_post_meta( $course_id, '_tutor_course_benefits', true ),
1345 'course_requirements' => get_post_meta( $course_id, '_tutor_course_requirements', true ),
1346 'course_target_audience' => get_post_meta( $course_id, '_tutor_course_target_audience', true ),
1347 'course_material_includes' => get_post_meta( $course_id, '_tutor_course_material_includes', true ),
1348 'monetize_by' => $monetize_by,
1349 'course_pricing' => $course_pricing,
1350 'course_settings' => get_post_meta( $course_id, '_tutor_course_settings', true ),
1351 'step_completion_status' => array(
1352 'basic' => true,
1353 'curriculum' => false,
1354 'additional' => false,
1355 'certificate' => false,
1356 ),
1357 );
1358
1359 $tax_on_single = get_post_meta( $course_id, self::TAX_ON_SINGLE_META, true );
1360 $tax_on_subscription = get_post_meta( $course_id, self::TAX_ON_SUBSCRIPTION_META, true );
1361
1362 $data['tax_collection'] = array(
1363 'tax_on_single' => '' === $tax_on_single ? '1' : $tax_on_single,
1364 'tax_on_subscription' => '' === $tax_on_subscription ? '1' : $tax_on_subscription,
1365 );
1366
1367 $data = apply_filters( 'tutor_course_details_response', array_merge( $course, $data ) );
1368
1369 $this->json_response( __( 'Data retrieved successfully!', 'tutor' ), $data );
1370 }
1371
1372 /**
1373 * Load course builder.
1374 *
1375 * @since 3.0.0
1376 *
1377 * @return void
1378 */
1379 public function load_course_builder() {
1380 global $pagenow;
1381
1382 $has_pro = tutor()->has_pro;
1383 $has_access_role = User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) );
1384
1385 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1386 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1387 $backend_edit = $backend_builder && $course_id;
1388
1389 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1390 $frontend_edit = $is_frontend_builder && $course_id;
1391
1392 if ( $has_access_role && ( $backend_edit || ( $has_pro && $frontend_edit ) ) ) {
1393 $post_type = get_post_type( $course_id );
1394 $can_edit_course = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
1395
1396 if ( tutor()->course_post_type === $post_type && ( User::is_admin() || $can_edit_course ) ) {
1397 /**
1398 * Edit trash course behavior
1399 *
1400 * @since 3.0.0
1401 */
1402 if ( CourseModel::STATUS_TRASH === get_post_status( $course_id ) ) {
1403 $message = User::is_admin()
1404 ? __( 'You cannot edit this course because it is in the Trash. Please restore it and try again', 'tutor' )
1405 : tutor_utils()->error_message();
1406 wp_die( esc_html( $message ) );
1407 }
1408
1409 $this->load_course_builder_view();
1410 }
1411 }
1412 }
1413
1414 /**
1415 * Enqueue course builder assets like CSS, JS
1416 *
1417 * @since 3.0.0
1418 *
1419 * @return void
1420 */
1421 public function enqueue_course_builder_assets() {
1422 // Fix: function print_emoji_styles is deprecated since version 6.4.0!
1423 remove_action( 'wp_print_styles', 'print_emoji_styles' );
1424 remove_action( 'wp_head', 'wp_admin_bar_header' );
1425 add_action( 'wp_head', 'wp_enqueue_admin_bar_header_styles' );
1426
1427 do_action( 'tutor_course_builder_before_wp_editor_load' );
1428 wp_enqueue_script( 'wp-tinymce' );
1429 wp_enqueue_script( 'mce-view' );
1430 wp_enqueue_editor();
1431
1432 wp_enqueue_media();
1433 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 );
1434 wp_set_script_translations( 'tutor-course-builder', 'tutor', tutor()->path . 'languages/' );
1435
1436 wp_localize_script(
1437 'mce-view',
1438 'mceViewL10n',
1439 array(
1440 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1441 )
1442 );
1443 }
1444
1445 /**
1446 * Localize custom course builder data for _tutorobject.
1447 *
1448 * @since 3.3.1
1449 *
1450 * @param array $data the localized data.
1451 *
1452 * @return array
1453 */
1454 public function localize_course_builder_data( $data ) {
1455 global $pagenow;
1456
1457 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1458 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1459 $backend_edit = $backend_builder && $course_id;
1460
1461 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1462 $frontend_edit = $is_frontend_builder && $course_id;
1463
1464 if ( ! $backend_edit && ! $frontend_edit ) {
1465 return $data;
1466 }
1467
1468 /**
1469 * Prepare course builder data.
1470 */
1471 $default_data = ( new Assets( false ) )->get_default_localized_data();
1472
1473 if ( isset( $default_data['current_user']['data']['id'] ) ) {
1474 $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1475 $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
1476 }
1477
1478 /**
1479 * Localized only options to protect sensitive info like API keys.
1480 */
1481 $required_options = array(
1482 'monetize_by',
1483 'enable_course_marketplace',
1484 'course_permalink_base',
1485 'supported_video_sources',
1486 'enrollment_expiry_enabled',
1487 'enable_q_and_a_on_course',
1488 'instructor_can_delete_course',
1489 'chatgpt_enable',
1490 'hide_admin_bar_for_users',
1491 'enable_redirect_on_course_publish_from_frontend',
1492 'instructor_can_publish_course',
1493 'instructor_can_change_course_author',
1494 'instructor_can_manage_co_instructors',
1495 );
1496
1497 $full_settings = get_option( 'tutor_option', array() );
1498 $settings = Options_V2::get_only( $required_options );
1499 $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1500 $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1501 $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1502
1503 $settings['enable_tax'] = Tax::get_setting( 'enable_tax', true );
1504 $settings['is_tax_included_in_price'] = Tax::is_tax_included_in_price();
1505 $settings['enable_individual_tax_control'] = Tax::get_setting( 'enable_individual_tax_control' );
1506
1507 $new_data = array( 'settings' => $settings );
1508
1509 $data = array_merge( $default_data, $new_data );
1510
1511 /**
1512 * Course builder dashboard URL based on role and settings.
1513 */
1514 $dashboard_url = tutor_utils()->tutor_dashboard_url();
1515 if ( User::is_admin() ) {
1516 $dashboard_url = get_admin_url();
1517 }
1518
1519 /**
1520 * EDD product list
1521 */
1522 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1523 if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1524 $data['edd_products'] = tutor_utils()->get_edd_products();
1525 }
1526
1527 $difficulty_levels = array();
1528 foreach ( tutor_utils()->course_levels() as $value => $label ) {
1529 $difficulty_levels[] = array(
1530 'label' => $label,
1531 'value' => $value,
1532 );
1533 }
1534
1535 $supported_video_sources = array();
1536 $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1537
1538 foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1539 if ( in_array( $value, $saved_video_source_list, true ) ) {
1540 $supported_video_sources[] = array(
1541 'label' => $label,
1542 'value' => $value,
1543 );
1544 }
1545 }
1546
1547 $data['dashboard_url'] = $dashboard_url;
1548 $data['backend_course_list_url'] = get_admin_url( null, 'admin.php?page=tutor' );
1549 $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1550 $data['timezones'] = tutor_global_timezone_lists();
1551 $data['difficulty_levels'] = $difficulty_levels;
1552 $data['supported_video_sources'] = $supported_video_sources;
1553 $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1554
1555 if ( 'en_US' !== $data['local'] ) {
1556 $data['course_builder_basic_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-basic', $data['local'] );
1557 $data['course_builder_curriculum_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-curriculum', $data['local'] );
1558 $data['course_builder_additional_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-additional', $data['local'] );
1559 }
1560
1561 $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1562
1563 return $data;
1564 }
1565
1566 /**
1567 * Load view for course builder.
1568 *
1569 * @since 3.0.0
1570 *
1571 * @return void
1572 */
1573 public function load_course_builder_view() {
1574 /**
1575 * Hide admin menu and footer.
1576 *
1577 * @since 3.3.0
1578 */
1579 echo '<style>
1580 #adminmenumain, #wpfooter, .notice, #tutor-page-wrap { display: none !important; }
1581 #wpcontent { margin: 0 !important; padding: 0 !important; }
1582 #wpbody-content { padding-bottom: 0px !important; float: none; }
1583 </style>';
1584
1585 do_action( 'tutor_before_course_builder_load' );
1586 include_once tutor()->path . 'views/pages/course-builder.php';
1587 do_action( 'tutor_after_course_builder_load' );
1588 }
1589
1590 /**
1591 * Add enroll require login class
1592 *
1593 * @since 2.6.0
1594 *
1595 * @param string $class_name css class name.
1596 *
1597 * @return string
1598 */
1599 public function add_enroll_required_login_class( $class_name ) {
1600 $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1601 if ( ! $enabled_tutor_login ) {
1602 return '';
1603 }
1604
1605 return $class_name;
1606 }
1607
1608 /**
1609 * Get list of WC products.
1610 *
1611 * @since 2.5.0
1612 * @since 3.0.0 exclude_linked_products, course_id are added.
1613 *
1614 * @return void
1615 */
1616 public function get_wc_products() {
1617 $exclude = array();
1618 $exclude_linked_products = Input::has( 'exclude_linked_products' );
1619 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1620
1621 if ( $exclude_linked_products ) {
1622 $exclude = tutor_utils()->get_linked_product_ids();
1623 }
1624
1625 if ( $course_id ) {
1626 $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1627 if ( $linked_product_id ) {
1628 $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1629 }
1630 }
1631
1632 $exclude = array_unique( $exclude );
1633
1634 $this->json_response(
1635 __( 'Products retrieved successfully!', 'tutor' ),
1636 tutor_utils()->get_wc_products_db( $exclude ),
1637 HttpHelper::STATUS_OK
1638 );
1639 }
1640
1641 /**
1642 * Get course associate WC product info by Ajax request
1643 *
1644 * @since 2.0.7
1645 *
1646 * @return void
1647 */
1648 public function get_wc_product() {
1649 tutor_utils()->checking_nonce();
1650 $product_id = Input::post( 'product_id' );
1651 $product = wc_get_product( $product_id );
1652 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1653
1654 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1655
1656 /**
1657 * If selected product is already linked with
1658 * a course & it is not the current course the
1659 * return error
1660 *
1661 * @since 2.1.0
1662 */
1663 if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1664 wp_send_json_error(
1665 __( 'One product can not be added to multiple course!', 'tutor' )
1666 );
1667 }
1668
1669 if ( $product ) {
1670 $data = array(
1671 'name' => $product->get_name(),
1672 'regular_price' => $product->get_regular_price(),
1673 'sale_price' => $product->get_sale_price(),
1674 );
1675 wp_send_json_success( $data );
1676 } else {
1677 wp_send_json_error( __( 'Product not found', 'tutor' ) );
1678 }
1679 }
1680
1681 /**
1682 * Update course content order
1683 *
1684 * @since 1.0.0
1685 * @return void
1686 */
1687 public function tutor_update_course_content_order() {
1688 tutor_utils()->checking_nonce();
1689
1690 if ( Input::has( 'content_parent' ) ) {
1691 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1692 $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1693 $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1694
1695 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1696 wp_send_json_success( array( 'message' => __( 'Access Denied!', 'tutor' ) ) );
1697 exit;
1698 }
1699
1700 // Update the parent topic id of the content.
1701 global $wpdb;
1702 $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1703 }
1704
1705 // Save course content order.
1706 $this->save_course_content_order();
1707
1708 wp_send_json_success();
1709 }
1710
1711 /**
1712 * Restrict new student entry
1713 *
1714 * @since 1.0.0
1715 * @param mixed $content content.
1716 *
1717 * @return mixed
1718 */
1719 public function restrict_new_student_entry( $content ) {
1720
1721 if ( ! tutor_utils()->is_course_fully_booked() ) {
1722 // No restriction if not fully booked.
1723 return $content;
1724 }
1725
1726 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">' .
1727 __( 'Fully Booked', 'tutor' )
1728 . '</div></div>';
1729 }
1730
1731 /**
1732 * Restrict media
1733 *
1734 * @since 1.0.0
1735 * @param string $where where clause.
1736 * @return string
1737 */
1738 public function restrict_media( $where ) {
1739 $action = Input::post( 'action' );
1740 if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1741 if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1742 $where .= ' AND post_author=' . get_current_user_id();
1743 }
1744 }
1745
1746 return $where;
1747 }
1748
1749 /**
1750 * Save course content order
1751 *
1752 * @since 1.0.0
1753 * @return void
1754 */
1755 private function save_course_content_order() {
1756 global $wpdb;
1757
1758 $new_order = Input::post( 'tutor_topics_lessons_sorting' );
1759 if ( ! empty( $new_order ) ) {
1760 $order = json_decode( $new_order, true );
1761
1762 if ( is_array( $order ) && count( $order ) ) {
1763 $i = 0;
1764 foreach ( $order as $topic ) {
1765 $i++;
1766 $wpdb->update(
1767 $wpdb->posts,
1768 array( 'menu_order' => $i ),
1769 array( 'ID' => $topic['topic_id'] )
1770 );
1771
1772 /**
1773 * Removing All lesson with topic
1774 */
1775
1776 $wpdb->update(
1777 $wpdb->posts,
1778 array( 'post_parent' => 0 ),
1779 array( 'post_parent' => $topic['topic_id'] )
1780 );
1781
1782 /**
1783 * Lesson Attaching with topic ID
1784 * Sorting lesson
1785 */
1786 if ( isset( $topic['lesson_ids'] ) ) {
1787 $lesson_ids = $topic['lesson_ids'];
1788 } else {
1789 $lesson_ids = array();
1790 }
1791 if ( count( $lesson_ids ) ) {
1792 foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1793 $wpdb->update(
1794 $wpdb->posts,
1795 array(
1796 'post_parent' => $topic['topic_id'],
1797 'menu_order' => $lesson_key,
1798 ),
1799 array( 'ID' => $lesson_id )
1800 );
1801 }
1802 }
1803 }
1804 }
1805 }
1806 }
1807
1808 /**
1809 * Insert Topic and attached it with Course
1810 *
1811 * @since 1.0.0
1812 *
1813 * @param integer $post_ID post ID.
1814 * @param object $post post object.
1815 *
1816 * @return void
1817 */
1818 public function save_course_meta( $post_ID, $post ) {
1819 global $wpdb;
1820
1821 do_action( 'tutor_save_course', $post_ID, $post );
1822
1823 /**
1824 * Save course price type
1825 */
1826 $price_type = Input::post( 'tutor_course_price_type' );
1827 if ( $price_type ) {
1828 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
1829 }
1830
1831 //phpcs:disable WordPress.Security.NonceVerification.Missing
1832 // Course Duration.
1833 if ( ! empty( $_POST['course_duration'] ) ) {
1834 $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1835 update_post_meta( $post_ID, '_course_duration', $video );
1836 }
1837
1838 if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1839 $course_level = Input::post( '_tutor_course_level' );
1840 update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1841 }
1842
1843 $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1844 if ( $additional_data_edit ) {
1845 if ( ! empty( $_POST['course_benefits'] ) ) {
1846 $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1847 update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1848 } elseif ( ! tutor_is_rest() ) {
1849 delete_post_meta( $post_ID, '_tutor_course_benefits' );
1850 }
1851
1852 if ( ! empty( $_POST['course_requirements'] ) ) {
1853 $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1854 update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1855 } elseif ( ! tutor_is_rest() ) {
1856 delete_post_meta( $post_ID, '_tutor_course_requirements' );
1857 }
1858
1859 if ( ! empty( $_POST['course_target_audience'] ) ) {
1860 $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1861 update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1862 } elseif ( ! tutor_is_rest() ) {
1863 delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1864 }
1865
1866 if ( ! empty( $_POST['course_material_includes'] ) ) {
1867 $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1868 update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1869 } elseif ( ! tutor_is_rest() ) {
1870 delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1871 }
1872 //phpcs:enable WordPress.Security.NonceVerification.Missing
1873 }
1874
1875 /**
1876 * Sorting Topics and lesson
1877 */
1878 $this->save_course_content_order();
1879
1880 // Additional data like course intro video.
1881 if ( $additional_data_edit ) {
1882 // Sanitize data through helper method.
1883 $video = Input::sanitize_array(
1884 $_POST['video'] ?? array(), //phpcs:ignore
1885 array(
1886 'source_external_url' => 'esc_url',
1887 'source_embedded' => 'wp_kses_post',
1888 ),
1889 true
1890 );
1891 $video_source = tutor_utils()->array_get( 'source', $video );
1892 if ( -1 !== $video_source ) {
1893 update_post_meta( $post_ID, '_video', $video );
1894 } elseif ( ! tutor_is_rest() ) {
1895 delete_post_meta( $post_ID, '_video' );
1896 }
1897 }
1898
1899 /**
1900 * Adding author to instructor automatically
1901 */
1902
1903 // Override post author id.
1904 $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1905 $attached = (int) $wpdb->get_var(
1906 $wpdb->prepare(
1907 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1908 WHERE user_id = %d
1909 AND meta_key = '_tutor_instructor_course_id'
1910 AND meta_value = %d ",
1911 $author_id,
1912 $post_ID
1913 )
1914 );
1915
1916 if ( ! $attached ) {
1917 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1918 }
1919
1920 /**
1921 * Disable question and answer for this course
1922 *
1923 * @since 1.7.0
1924 */
1925 if ( $additional_data_edit ) {
1926 foreach ( $this->additional_meta as $key ) {
1927 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1928 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1929 }
1930 }
1931
1932 do_action( 'tutor_save_course_after', $post_ID, $post );
1933 }
1934
1935 /**
1936 * Save course topic
1937 *
1938 * @since 1.0.0
1939 * @since 3.0.0 response and input name updated.
1940 *
1941 * @return void
1942 */
1943 public function tutor_save_topic() {
1944 tutor_utils()->check_nonce();
1945
1946 $is_update = false;
1947 $errors = array();
1948 $topic_title = Input::post( 'title' );
1949
1950 if ( empty( $topic_title ) ) {
1951 $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
1952 $this->json_response(
1953 __( 'Invalid inputs', 'tutor' ),
1954 $errors,
1955 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
1956 );
1957 }
1958
1959 // Gather parameters.
1960 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1961 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1962 $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
1963
1964 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
1965
1966 // Validate if user can manage the topic.
1967 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
1968 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1969 }
1970
1971 // Create payload to create/update the topic.
1972 $post_arr = array(
1973 'post_type' => 'topics',
1974 'post_title' => $topic_title,
1975 'post_content' => $topic_summary,
1976 'post_status' => 'publish',
1977 'post_author' => get_current_user_id(),
1978 'post_parent' => $course_id,
1979 'menu_order' => $next_topic_order_id,
1980 );
1981
1982 if ( $topic_id ) {
1983 $is_update = true;
1984 $post_arr['ID'] = $topic_id;
1985 }
1986
1987 $current_topic_id = wp_insert_post( $post_arr );
1988
1989 if ( $is_update ) {
1990 $this->json_response(
1991 __( 'Topic updated successfully!', 'tutor' ),
1992 $current_topic_id
1993 );
1994 } else {
1995 $this->json_response(
1996 __( 'Topic created successfully!', 'tutor' ),
1997 $current_topic_id,
1998 HttpHelper::STATUS_CREATED
1999 );
2000 }
2001 }
2002
2003 /**
2004 * Delete a course topic
2005 *
2006 * @since 1.0.0
2007 * @since 3.0.0 code refactor and response updated.
2008 *
2009 * @return void
2010 */
2011 public function tutor_delete_topic() {
2012 tutor_utils()->check_nonce();
2013
2014 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2015 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
2016 $this->json_response(
2017 tutor_utils()->error_message(),
2018 null,
2019 HttpHelper::STATUS_FORBIDDEN
2020 );
2021 }
2022
2023 global $wpdb;
2024
2025 // Assign course ID to orphan content IDs since the topic will be deleted.
2026 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
2027 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
2028 foreach ( $content_ids as $content_id ) {
2029 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
2030 // Actually all kind of contents.
2031 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
2032 }
2033
2034 // Set contents under the topic orphan.
2035 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
2036
2037 // Then delete the topic from database.
2038 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
2039 wp_delete_post( $topic_id );
2040
2041 $this->json_response(
2042 __( 'Topic deleted successfully!', 'tutor' )
2043 );
2044 }
2045
2046 /**
2047 * Handle enroll now action
2048 *
2049 * @since 1.0.0
2050 *
2051 * @return void
2052 */
2053 public function enroll_now() {
2054
2055 if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
2056 return;
2057 }
2058
2059 // Checking Nonce.
2060 tutor_utils()->checking_nonce();
2061
2062 $user_id = get_current_user_id();
2063 if ( ! $user_id ) {
2064 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
2065 }
2066
2067 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
2068
2069 /**
2070 * TODO: need to check purchase information
2071 */
2072
2073 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2074
2075 /**
2076 * If is is not purchasable, it's free, and enroll right now
2077 * If purchasable, then process purchase.
2078 *
2079 * @since: v.1.0.0
2080 */
2081 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2082 // Process purchase.
2083
2084 } else {
2085 // Free enroll.
2086 tutor_utils()->do_enroll( $course_id );
2087 }
2088
2089 $referer_url = wp_get_referer();
2090 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2091 exit;
2092 }
2093
2094 /**
2095 * Mark complete completed
2096 *
2097 * @since 1.0.0
2098 *
2099 * @since 3.7.1 Filter hook: tutor_user_can_complete_course added
2100 *
2101 * @return void
2102 */
2103 public function mark_course_complete() {
2104 $tutor_action = Input::post( 'tutor_action' );
2105 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2106 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
2107 return;
2108 }
2109
2110 $permalink = get_the_permalink( $course_id );
2111
2112 // Checking nonce.
2113 tutor_utils()->checking_nonce();
2114
2115 $user_id = get_current_user_id();
2116
2117 // TODO: need to show view if not signed_in.
2118 if ( ! $user_id ) {
2119 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2120 }
2121
2122 if ( ! tutor_utils()->is_enrolled( $course_id, $user_id ) ) {
2123 die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2124 }
2125
2126 /**
2127 * Filter hook provided to restrict course completion. This is useful
2128 * for specific cases like prerequisites. WP_Error should be returned
2129 * from the filter value to prevent the completion.
2130 */
2131 $can_complete = apply_filters( 'tutor_user_can_complete_course', true, $user_id, $course_id );
2132
2133 if ( is_wp_error( $can_complete ) ) {
2134 tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2135 } else {
2136 CourseModel::mark_course_as_completed( $course_id, $user_id );
2137 // Set temporary identifier to show review pop up.
2138 self::set_review_popup_data( $user_id, $course_id, $permalink );
2139
2140 wp_safe_redirect( $permalink );
2141 exit;
2142 }
2143 }
2144
2145 /**
2146 * Set data for review popup.
2147 *
2148 * @since 2.2.5
2149 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2150 *
2151 * @param int $user_id user id.
2152 * @param int $course_id course id.
2153 *
2154 * @return void
2155 */
2156 public static function set_review_popup_data( $user_id, $course_id ) {
2157 if ( get_tutor_option( 'enable_course_review' ) ) {
2158 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2159 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2160 $meta_key = User::get_review_popup_meta( $course_id );
2161 add_user_meta( $user_id, $meta_key, $course_id, true );
2162 }
2163 }
2164 }
2165
2166 /**
2167 * Popup review form on course details
2168 *
2169 * @since 1.0.0
2170 * @return void
2171 */
2172 public function popup_review_form() {
2173 if ( is_user_logged_in() ) {
2174 $user_id = get_current_user_id();
2175 $course_id = get_the_ID();
2176 $meta_key = User::get_review_popup_meta( $course_id );
2177 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2178
2179 if ( is_single() && $course_id === $review_course_id ) {
2180 include tutor()->path . 'views/modal/review.php';
2181 }
2182 }
2183 }
2184
2185 /**
2186 * Review popup data clear
2187 *
2188 * @since 2.4.0
2189 *
2190 * @return void
2191 */
2192 public function clear_review_popup_data() {
2193 tutils()->checking_nonce();
2194
2195 if ( is_user_logged_in() ) {
2196 $user_id = get_current_user_id();
2197 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2198
2199 if ( $course_id ) {
2200 $meta_key = User::get_review_popup_meta( $course_id );
2201 delete_user_meta( $user_id, $meta_key, $course_id );
2202 }
2203
2204 wp_send_json_success();
2205 }
2206 }
2207
2208 /**
2209 * Delete course delete from frontend dashboard
2210 *
2211 * @since 2.0.0
2212 * @return void
2213 */
2214 public function tutor_delete_dashboard_course() {
2215 tutor_utils()->checking_nonce();
2216
2217 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2218 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2219 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2220 }
2221
2222 /**
2223 * Co-instructor can not delete a course
2224 *
2225 * @since 2.1.6
2226 */
2227 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2228 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2229 }
2230
2231 // Check if user is only an instructor.
2232 if ( ! current_user_can( 'administrator' ) ) {
2233 // Check if instructor can trash course.
2234 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2235
2236 if ( ! $can_trash_post ) {
2237 wp_send_json_error( tutor_utils()->error_message() );
2238 }
2239 }
2240
2241 $trash_course = wp_update_post(
2242 array(
2243 'ID' => $course_id,
2244 'post_status' => 'trash',
2245 )
2246 );
2247
2248 if ( $trash_course ) {
2249 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2250 }
2251 wp_send_json_success();
2252 }
2253
2254 /**
2255 * Main author change from gutenberg editor
2256 *
2257 * @since 2.0.0
2258 *
2259 * @param array $data data.
2260 * @param array $postarr post array.
2261 *
2262 * @return mixed
2263 */
2264 public function tutor_add_gutenberg_author( $data, $postarr ) {
2265 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2266 $post_type = $postarr['post_type'];
2267 $courses_post_type = tutor()->course_post_type;
2268
2269 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2270 return $data;
2271 }
2272
2273 /**
2274 * Only admin can change main author
2275 */
2276 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2277 global $wpdb;
2278 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2279 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2280
2281 if ( $post_author > 0 ) {
2282 $data['post_author'] = $post_author;
2283 } else {
2284 $data['post_author'] = get_current_user_id();
2285 }
2286 }
2287
2288 return $data;
2289 }
2290
2291
2292 /**
2293 * Attach product with course when course save from frontend or backend.
2294 *
2295 * @since 1.3.4
2296 *
2297 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2298 *
2299 * @param integer $post_ID course ID.
2300 * @param array $post_data created course post details.
2301 *
2302 * @return void
2303 */
2304 public function attach_product_with_course( $post_ID, $post_data ) {
2305 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2306 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2307
2308 /**
2309 * For native monetization, just return
2310 * No need to attach anything.
2311 */
2312 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2313 return;
2314 }
2315
2316 /**
2317 * When course moved paid to free
2318 * Keep the product linked and return.
2319 */
2320 if ( -1 === $product_id ) {
2321 return;
2322 }
2323
2324 /**
2325 * Free user can only select product from dropdown
2326 */
2327 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2328 if ( $product_id > 0 ) {
2329 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2330 }
2331
2332 return;
2333 }
2334
2335 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2336 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2337 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2338
2339 if ( ! $course_price || $sale_price >= $course_price ) {
2340 return;
2341 }
2342
2343 $course = get_post( $post_ID );
2344
2345 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2346
2347 if ( 'wc' === $monetize_by ) {
2348
2349 $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2350
2351 if ( $is_update ) {
2352 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2353
2354 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2355 $product_obj = wc_get_product( $product_id );
2356 if ( $product_obj->is_type( 'subscription' ) ) {
2357 update_post_meta( $product_id, '_subscription_price', $course_price );
2358 }
2359
2360 // Set course regular & sale price.
2361 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2362 } else {
2363 // Create new WC product name with course title.
2364 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2365 if ( $product_id ) {
2366 $product_obj = wc_get_product( $product_id );
2367
2368 self::sync_course_with_wc_product( $post_ID, $product_id );
2369
2370 // Set course regular & sale price.
2371 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2372 }
2373 }
2374
2375 $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2376 if ( $product_id && $course_post_thumbnail ) {
2377 set_post_thumbnail( $product_id, $course_post_thumbnail );
2378 }
2379 } elseif ( 'edd' === $monetize_by ) {
2380
2381 $is_update = false;
2382
2383 if ( $attached_product_id ) {
2384 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2385 if ( $edd_price ) {
2386 $is_update = true;
2387 }
2388 }
2389
2390 if ( $is_update ) {
2391 // Update the product.
2392 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2393 } else {
2394 // Create new product.
2395
2396 $post_arr = array(
2397 'post_type' => 'download',
2398 'post_title' => $course->post_title,
2399 'post_status' => 'publish',
2400 'post_author' => get_current_user_id(),
2401 );
2402 $download_id = wp_insert_post( $post_arr );
2403 if ( $download_id ) {
2404 // EDD edd_price.
2405 update_post_meta( $download_id, 'edd_price', $course_price );
2406
2407 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2408 // Mark product for EDD.
2409 update_post_meta( $download_id, '_tutor_product', 'yes' );
2410
2411 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2412 if ( $course_post_thumbnail ) {
2413 set_post_thumbnail( $download_id, $course_post_thumbnail );
2414 }
2415 }
2416 }
2417 }
2418 }
2419
2420 /**
2421 * Add Course level to course settings
2422 *
2423 * @since 1.4.1
2424 *
2425 * @param array $args arguments.
2426 * @return array
2427 */
2428 public function add_course_level_to_settings( $args ) {
2429 $course_id = get_the_ID();
2430 $levels = tutor_utils()->course_levels();
2431 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2432
2433 $args['general']['fields']['_tutor_course_level'] = array(
2434 'type' => 'select',
2435 'label' => __( 'Difficulty Level', 'tutor' ),
2436 'label_title' => __( 'Enable', 'tutor' ),
2437 'options' => $levels,
2438 'value' => $course_level ? $course_level : 'intermediate',
2439 'desc' => __( 'Course difficulty level', 'tutor' ),
2440 );
2441
2442 return $args;
2443 }
2444
2445 /**
2446 * Check if course starting
2447 *
2448 * @since 1.4.8
2449 * @return void
2450 */
2451 public function tutor_lesson_load_before() {
2452 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2453 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2454 if ( is_user_logged_in() ) {
2455 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2456 if ( ! $completed_lessons && ! $is_course_started ) {
2457 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2458 do_action( 'tutor/course/started', $course_id );
2459 }
2460 }
2461 }
2462
2463 /**
2464 * Add Course level to course settings
2465 *
2466 * @since 1.4.8
2467 * @return void
2468 */
2469 public function course_elements_enable_disable() {
2470 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2471 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2472 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2473 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2474 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2475 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2476 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2477 }
2478
2479 /**
2480 * Enable disable course progress bar
2481 *
2482 * @since 1.4.8
2483 *
2484 * @param string $html HTML string.
2485 * @return string
2486 */
2487 public function enable_disable_course_progress_bar( $html ) {
2488 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2489 if ( $disable_option ) {
2490 return '';
2491 }
2492 return $html;
2493 }
2494
2495 /**
2496 * Enable disable material includes
2497 *
2498 * @since 1.4.8
2499 *
2500 * @param string $html HTML string.
2501 * @return string
2502 */
2503 public function enable_disable_material_includes( $html ) {
2504 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2505 if ( $disable_option ) {
2506 return '';
2507 }
2508 return $html;
2509 }
2510
2511 /**
2512 * Enable disable course content
2513 *
2514 * @since 1.4.8
2515 *
2516 * @param string $html HTML string.
2517 * @return string
2518 */
2519 public function enable_disable_course_content( $html ) {
2520 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2521 if ( $disable_option ) {
2522 return '';
2523 }
2524 return $html;
2525 }
2526
2527 /**
2528 * Enable disable course benefits
2529 *
2530 * @since 1.4.8
2531 *
2532 * @param string $html HTML string.
2533 * @return string
2534 */
2535 public function enable_disable_course_benefits( $html ) {
2536 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2537 if ( $disable_option ) {
2538 return '';
2539 }
2540 return $html;
2541 }
2542
2543 /**
2544 * Enable disable course requirements
2545 *
2546 * @since 1.4.8
2547 *
2548 * @param string $html HTML string.
2549 * @return string
2550 */
2551 public function enable_disable_course_requirements( $html ) {
2552 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2553 if ( $disable_option ) {
2554 return '';
2555 }
2556 return $html;
2557 }
2558
2559 /**
2560 * Enable disable course target audience
2561 *
2562 * @since 1.4.8
2563 *
2564 * @param string $html HTML string.
2565 * @return string
2566 */
2567 public function enable_disable_course_target_audience( $html ) {
2568 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2569 if ( $disable_option ) {
2570 return '';
2571 }
2572 return $html;
2573 }
2574
2575 /**
2576 * Enable disable course nav items
2577 *
2578 * @since 1.4.8
2579 *
2580 * @param array $items item list.
2581 * @param integer $course_id course ID.
2582 *
2583 * @return array
2584 */
2585 public function enable_disable_course_nav_items( $items, $course_id ) {
2586 global $wp_query, $post;
2587 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2588 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2589 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2590
2591 // Whether Q&A enabled.
2592 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2593 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2594 unset( $items['questions'] );
2595 }
2596 }
2597
2598 // Whether announcment enabled.
2599 if ( $disable_course_announcements ) {
2600 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2601 unset( $items['announcements'] );
2602 }
2603 }
2604
2605 // Hide review section if disabled.
2606 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2607 unset( $items['reviews'] );
2608 }
2609
2610 // Whether enrollment require.
2611 $is_enrolled = tutor_utils()->is_enrolled();
2612
2613 return array_filter(
2614 $items,
2615 function ( $item ) use ( $is_enrolled ) {
2616 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2617 return $is_enrolled;
2618 }
2619 return true;
2620 }
2621 );
2622 }
2623
2624 /**
2625 * Filter product in shop page
2626 *
2627 * @since 1.4.9
2628 * @return void|null
2629 */
2630 public function filter_product_in_shop_page() {
2631 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2632 if ( ! $hide_course_from_shop_page ) {
2633 return;
2634 }
2635 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2636 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2637 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2638 }
2639
2640
2641 /**
2642 * Tutor product meta query
2643 *
2644 * @since 1.4.9
2645 * @return array
2646 */
2647 public function tutor_product_meta_query() {
2648 $meta_query = array(
2649 'key' => '_tutor_product',
2650 'compare' => 'NOT EXISTS',
2651 );
2652 return $meta_query;
2653 }
2654
2655 /**
2656 * Filter product in woocommerce shop page
2657 *
2658 * @since 1.4.9
2659 *
2660 * @param \WP_Query $wp_query WP Query instance.
2661 * @return \WP_Query
2662 */
2663 public function filter_woocommerce_product_query( $wp_query ) {
2664 $product_ids = $this->get_connected_wc_product_ids();
2665 $wp_query->set( 'post__not_in', $product_ids );
2666 return $wp_query;
2667 }
2668
2669 /**
2670 * Get connected woocommerce product ids for course and course bundle
2671 *
2672 * @since 2.7.2
2673 *
2674 * @return array
2675 */
2676 public function get_connected_wc_product_ids() {
2677 global $wpdb;
2678
2679 $results = $wpdb->get_results(
2680 $wpdb->prepare(
2681 "SELECT DISTINCT pm.meta_value product_id
2682 FROM {$wpdb->posts} p
2683 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2684 AND pm.meta_key = %s
2685 WHERE post_type IN( 'courses','course-bundle' )",
2686 '_tutor_course_product_id'
2687 )
2688 );
2689
2690 $ids = array();
2691 if ( is_array( $results ) && count( $results ) ) {
2692 $ids = array_column( $results, 'product_id' );
2693 }
2694
2695 return $ids;
2696 }
2697
2698 /**
2699 * Filter product in edd downloads shortcode page
2700 *
2701 * @since 1.4.9
2702 *
2703 * @param \WP_Query $query WP Query instance.
2704 * @return \WP_Query
2705 */
2706 public function filter_edd_downloads_query( $query ) {
2707 $query['meta_query'][] = $this->tutor_product_meta_query();
2708 return $query;
2709 }
2710
2711 /**
2712 * Filter product in edd downloads archive page
2713 *
2714 * @since 1.4.9
2715 *
2716 * @param \WP_Query $wp_query WP Query instance.
2717 * @return \WP_Query
2718 */
2719 public function filter_archive_meta_query( $wp_query ) {
2720 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2721 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2722 }
2723 return $wp_query;
2724 }
2725
2726 /**
2727 * Removed course price if already enrolled at single course
2728 *
2729 * @since 1.5.8
2730 *
2731 * @param string $html HTML string.
2732 * @return string
2733 */
2734 public function remove_price_if_enrolled( $html ) {
2735 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2736
2737 if ( $should_removed ) {
2738 $course_id = get_the_ID();
2739 $enrolled = tutor_utils()->is_enrolled( $course_id );
2740 if ( $enrolled ) {
2741 $html = '';
2742 }
2743 }
2744 return $html;
2745 }
2746
2747 /**
2748 * Check if all lessons and quizzes done before mark course complete.
2749 *
2750 * @since 1.5.8
2751 *
2752 * @param string $html HTML string.
2753 * @return string
2754 */
2755 public function tutor_lms_hide_course_complete_btn( $html ) {
2756
2757 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2758 if ( 'strict' !== $completion_mode ) {
2759 return $html;
2760 }
2761
2762 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2763 $lesson_count = tutor_utils()->get_lesson_count_by_course();
2764
2765 if ( $completed_lesson < $lesson_count ) {
2766 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2767 <div class="tutor-alert-text">
2768 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2769 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2770 </div>
2771 </div>';
2772 }
2773
2774 $quizzes = array();
2775 $assignments = array();
2776
2777 $course_contents = tutor_utils()->get_course_contents_by_id();
2778 if ( tutor_utils()->count( $course_contents ) ) {
2779 foreach ( $course_contents as $content ) {
2780 if ( 'tutor_quiz' === $content->post_type ) {
2781 $quizzes[] = $content;
2782 }
2783 if ( 'tutor_assignments' === $content->post_type ) {
2784 $assignments[] = $content;
2785 }
2786 }
2787 }
2788
2789 $required_assignment_pass = 0;
2790
2791 foreach ( $assignments as $row ) {
2792
2793 $assignment_submission = tutor_utils()->is_assignment_submitted( $row->ID );
2794 $is_reviewed_by_instructor = ! count( $assignment_submission )
2795 ? false
2796 : get_comment_meta( $assignment_submission[0]->comment_ID, 'evaluate_time', true );
2797
2798 if ( $assignment_submission && $is_reviewed_by_instructor ) {
2799 $pass_mark = tutor_utils()->get_assignment_option( $row->ID, 'pass_mark' );
2800 $has_passed = false;
2801 foreach ( $assignment_submission as $submission ) {
2802 $given_mark = (int) get_comment_meta( $submission->comment_ID, 'assignment_mark', true );
2803 if ( $given_mark >= $pass_mark ) {
2804 $has_passed = true;
2805 break;
2806 }
2807 }
2808 if ( ! $has_passed ) {
2809 $required_assignment_pass++;
2810 }
2811 } else {
2812 $required_assignment_pass++;
2813 }
2814 }
2815
2816 $is_quiz_pass = true;
2817 $required_quiz_pass = 0;
2818
2819 if ( tutor_utils()->count( $quizzes ) ) {
2820 foreach ( $quizzes as $quiz ) {
2821
2822 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2823 if ( $attempt ) {
2824 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2825 $earned_percentage = QuizModel::calculate_attempt_earned_percentage( $attempt );
2826
2827 if ( $earned_percentage < $passing_grade ) {
2828 $required_quiz_pass++;
2829 $is_quiz_pass = false;
2830 }
2831 } else {
2832 $required_quiz_pass++;
2833 $is_quiz_pass = false;
2834 }
2835 }
2836 }
2837
2838 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2839 $_msg = '';
2840 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2841 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2842
2843 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2844 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2845 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2846 }
2847
2848 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2849 //phpcs:ignore
2850 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2851 }
2852
2853 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2854 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2855 $_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 );
2856 }
2857
2858 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2859 <div class="tutor-alert-text">
2860 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2861 <span>' . $_msg . '</span>
2862 </div>
2863 </div>';
2864 }
2865
2866 return $html;
2867 }
2868
2869 /**
2870 * Generate Gradebook
2871 *
2872 * @since 1.5.8
2873 *
2874 * @param string $html HTML string.
2875 * @return string
2876 */
2877 public function get_generate_greadbook( $html ) {
2878 if ( ! tutor_utils()->is_completed_course() ) {
2879 return '';
2880 }
2881 return $html;
2882 }
2883
2884 /**
2885 * Add social share content in header
2886 *
2887 * @since 1.6.3
2888 * @return void
2889 */
2890 public function social_share_content() {
2891 global $wp_query, $post;
2892 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2893 <!--Facebook-->
2894 <meta property="og:type" content="website"/>
2895 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2896 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2897 <!--Twitter-->
2898 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2899 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2900 <!--Google+-->
2901 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2902 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2903 <?php
2904 }
2905 }
2906
2907 /**
2908 * Delete associated enrollment
2909 *
2910 * @since 1.8.2
2911 *
2912 * @param integer $post_id post ID.
2913 * @return void
2914 */
2915 public function delete_associated_enrollment( $post_id ) {
2916 global $wpdb;
2917
2918 $enroll_id = $wpdb->get_var(
2919 $wpdb->prepare(
2920 "SELECT
2921 post_id
2922 FROM
2923 {$wpdb->postmeta}
2924 WHERE
2925 meta_key='_tutor_enrolled_by_order_id'
2926 AND meta_value = %d
2927 ",
2928 $post_id
2929 )
2930 );
2931
2932 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2933
2934 $course_id = get_post_field( 'post_parent', $enroll_id );
2935 $user_id = get_post_field( 'post_author', $enroll_id );
2936
2937 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2938 }
2939 }
2940
2941 /**
2942 * Reset course progress.
2943 *
2944 * @since 1.5.8
2945 * @return void
2946 */
2947 public function tutor_reset_course_progress() {
2948 tutor_utils()->checking_nonce();
2949 $course_id = Input::post( 'course_id' );
2950
2951 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2952 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2953 return;
2954 }
2955
2956 tutor_utils()->delete_course_progress( $course_id );
2957 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2958 }
2959
2960 /**
2961 * Do enroll if guest attempt to enroll and course is free
2962 *
2963 * @since 1.9.8
2964 *
2965 * @param integer $course_id course ID.
2966 * @param integer $user_id user ID.
2967
2968 * @return void
2969 */
2970 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
2971 $course_id = sanitize_text_field( $course_id );
2972 $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
2973
2974 if ( $course_id && $is_allowed ) {
2975 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2976 if ( ! $is_purchasable ) {
2977 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
2978 do_action( 'guest_attempt_after_enrollment', $course_id );
2979 }
2980 }
2981 }
2982
2983 /**
2984 * Handle course enrollment
2985 *
2986 * @since 2.1.0
2987 * @return void
2988 */
2989 public function course_enrollment() {
2990 tutor_utils()->checking_nonce();
2991
2992 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2993 $user_id = get_current_user_id();
2994
2995 if ( $course_id ) {
2996 $password_protected = post_password_required( $course_id );
2997 if ( $password_protected ) {
2998 wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
2999 }
3000
3001 /**
3002 * This check was added to address a security issue where users could
3003 * enroll in a course via an AJAX call without purchasing it.
3004 *
3005 * To prevent this, we now verify whether the course is paid.
3006 * Additionally, we check if the user is already enrolled, since
3007 * Tutor's default behavior enrolls users automatically upon purchase.
3008 *
3009 * @since 3.9.4
3010 */
3011 if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3012 $is_enrolled = (bool) tutor_utils()->is_enrolled( $course_id, $user_id );
3013
3014 if ( ! $is_enrolled ) {
3015 wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3016 }
3017 }
3018
3019 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
3020 if ( $enroll ) {
3021 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3022 } else {
3023 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3024 }
3025 } else {
3026 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
3027 }
3028 }
3029
3030 /**
3031 * After trash a course direct to the course list page
3032 *
3033 * @since 2.1.7
3034 *
3035 * @param integer $post_id int course id.
3036 *
3037 * @return void
3038 */
3039 public static function redirect_to_course_list_page( int $post_id ): void {
3040 $post = get_post( $post_id );
3041 if ( tutor()->course_post_type === $post->post_type ) {
3042 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
3043 if ( ! $is_gutenberg_enabled ) {
3044 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
3045 exit;
3046 }
3047 }
3048 }
3049
3050 /**
3051 * Create or update WooCommerce product
3052 *
3053 * If product id not set it will create new one.
3054 *
3055 * @since 2.2.0
3056 *
3057 * @param string $title product title.
3058 * @param string $reg_price product price.
3059 * @param string $sale_price product sale price.
3060 * @param int $product_id product ID.
3061 * @param string $status product status.
3062 *
3063 * @return integer Product id or return 0 if WC not exists
3064 */
3065 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
3066 if ( ! tutor_utils()->has_wc() ) {
3067 return 0;
3068 }
3069
3070 $product_obj = new \WC_Product();
3071 if ( $product_id ) {
3072 $product_obj = wc_get_product( $product_id );
3073 }
3074
3075 $product_obj->set_name( $title );
3076 $product_obj->set_status( $status );
3077 $product_obj->set_price( $reg_price );
3078 $product_obj->set_regular_price( $reg_price );
3079
3080 if ( $sale_price > 0 ) {
3081 $product_obj->set_sale_price( $sale_price );
3082 } else {
3083 $product_obj->set_sale_price( null );
3084 }
3085
3086 $product_obj->set_sold_individually( true );
3087
3088 return $product_obj->save();
3089 }
3090
3091 /**
3092 * Get course/bundle mini info
3093 *
3094 * @since 3.0.0
3095 *
3096 * @param object $post Course or bundle post.
3097 *
3098 * @return array
3099 */
3100 public static function get_mini_info( object $post ) {
3101 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3102 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3103 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3104 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
3105
3106 $info = array(
3107 'id' => $post->ID,
3108 'title' => $post->post_title,
3109 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3110 'is_purchasable' => $is_purchasable,
3111 'regular_price' => $regular_price,
3112 'sale_price' => $sale_price,
3113 );
3114
3115 $card_data = apply_filters( 'tutor_course_mini_info', $info, $post );
3116
3117 return $card_data;
3118 }
3119
3120 /**
3121 * Get course/bundle card data
3122 *
3123 * This method will return all data that contain in
3124 * course card
3125 *
3126 * @since 3.0.0
3127 *
3128 * @param object $post Course or bundle post.
3129 *
3130 * @return array
3131 */
3132 public static function get_card_data( object $post ) {
3133 $info = self::get_mini_info( $post );
3134
3135 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3136 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3137 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3138
3139 $card_data = apply_filters( 'tutor_course_card_data', $info, $post );
3140
3141 return $card_data;
3142 }
3143
3144 /**
3145 * Filter user list access for instructor
3146 *
3147 * @since 3.0.0
3148 *
3149 * @param bool $access access.
3150 *
3151 * @return bool
3152 */
3153 public function user_list_access_for_instructor( $access ) {
3154 $is_instructor = User::is_instructor();
3155 return $access || $is_instructor;
3156 }
3157
3158 /**
3159 * Filter user list args for instructor
3160 *
3161 * @since 3.0.0
3162 *
3163 * @param array $args args.
3164 *
3165 * @return array
3166 */
3167 public function user_list_args_for_instructor( $args ) {
3168 if ( User::is_instructor() ) {
3169 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3170 unset( $args['fields']['user_email'] );
3171 }
3172 }
3173
3174 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3175 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3176 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3177 }
3178
3179 return $args;
3180 }
3181
3182 /**
3183 * Get a list of possible course status.
3184 *
3185 * @since 3.6.2
3186 *
3187 * @return array
3188 */
3189 public static function course_status_list() {
3190 return array(
3191 'publish',
3192 'private',
3193 'draft',
3194 'trash',
3195 'pending',
3196 'future',
3197 );
3198 }
3199
3200 /**
3201 * Link a course/bundle post to a WooCommerce product.
3202 *
3203 * @since 3.8.2
3204 *
3205 * @param int $post_ID The WordPress post ID of the course.
3206 * @param int $product_id The WooCommerce product ID to associate with the course.
3207 * @return void
3208 */
3209 public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3210
3211 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
3212
3213 // Mark product for woocommerce.
3214 update_post_meta( $product_id, '_virtual', 'yes' );
3215 update_post_meta( $product_id, '_tutor_product', 'yes' );
3216 }
3217
3218 /**
3219 * Map Tutor's course prices to WooCommerce.
3220 *
3221 * @since 3.8.2
3222 *
3223 * @param int $post_ID The WordPress post ID of the course.
3224 * @param string|int|float $regular_price The regular price.
3225 * @param string|int|float $sale_price The sale price.
3226 * @return void
3227 */
3228 private static function set_course_regular_and_sale_price( $post_ID, $regular_price, $sale_price ) {
3229
3230 // Set course regular & sale price.
3231 update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3232 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3233 }
3234 }
3235