PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.1
Tutor LMS – eLearning and online course solution v3.9.1
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 8 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 9 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 9 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 9 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 9 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 8 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 9 months ago
Course.php
3205 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 /**
2123 * Filter hook provided to restrict course completion. This is useful
2124 * for specific cases like prerequisites. WP_Error should be returned
2125 * from the filter value to prevent the completion.
2126 */
2127 $can_complete = apply_filters( 'tutor_user_can_complete_course', true, $user_id, $course_id );
2128 if ( is_wp_error( $can_complete ) ) {
2129 tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2130 } else {
2131 CourseModel::mark_course_as_completed( $course_id, $user_id );
2132 // Set temporary identifier to show review pop up.
2133 self::set_review_popup_data( $user_id, $course_id, $permalink );
2134
2135 wp_safe_redirect( $permalink );
2136 exit;
2137 }
2138 }
2139
2140 /**
2141 * Set data for review popup.
2142 *
2143 * @since 2.2.5
2144 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2145 *
2146 * @param int $user_id user id.
2147 * @param int $course_id course id.
2148 *
2149 * @return void
2150 */
2151 public static function set_review_popup_data( $user_id, $course_id ) {
2152 if ( get_tutor_option( 'enable_course_review' ) ) {
2153 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2154 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2155 $meta_key = User::get_review_popup_meta( $course_id );
2156 add_user_meta( $user_id, $meta_key, $course_id, true );
2157 }
2158 }
2159 }
2160
2161 /**
2162 * Popup review form on course details
2163 *
2164 * @since 1.0.0
2165 * @return void
2166 */
2167 public function popup_review_form() {
2168 if ( is_user_logged_in() ) {
2169 $user_id = get_current_user_id();
2170 $course_id = get_the_ID();
2171 $meta_key = User::get_review_popup_meta( $course_id );
2172 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2173
2174 if ( is_single() && $course_id === $review_course_id ) {
2175 include tutor()->path . 'views/modal/review.php';
2176 }
2177 }
2178 }
2179
2180 /**
2181 * Review popup data clear
2182 *
2183 * @since 2.4.0
2184 *
2185 * @return void
2186 */
2187 public function clear_review_popup_data() {
2188 tutils()->checking_nonce();
2189
2190 if ( is_user_logged_in() ) {
2191 $user_id = get_current_user_id();
2192 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2193
2194 if ( $course_id ) {
2195 $meta_key = User::get_review_popup_meta( $course_id );
2196 delete_user_meta( $user_id, $meta_key, $course_id );
2197 }
2198
2199 wp_send_json_success();
2200 }
2201 }
2202
2203 /**
2204 * Delete course delete from frontend dashboard
2205 *
2206 * @since 2.0.0
2207 * @return void
2208 */
2209 public function tutor_delete_dashboard_course() {
2210 tutor_utils()->checking_nonce();
2211
2212 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2213 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2214 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2215 }
2216
2217 /**
2218 * Co-instructor can not delete a course
2219 *
2220 * @since 2.1.6
2221 */
2222 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2223 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2224 }
2225
2226 // Check if user is only an instructor.
2227 if ( ! current_user_can( 'administrator' ) ) {
2228 // Check if instructor can trash course.
2229 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2230
2231 if ( ! $can_trash_post ) {
2232 wp_send_json_error( tutor_utils()->error_message() );
2233 }
2234 }
2235
2236 $trash_course = wp_update_post(
2237 array(
2238 'ID' => $course_id,
2239 'post_status' => 'trash',
2240 )
2241 );
2242
2243 if ( $trash_course ) {
2244 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2245 }
2246 wp_send_json_success();
2247 }
2248
2249 /**
2250 * Main author change from gutenberg editor
2251 *
2252 * @since 2.0.0
2253 *
2254 * @param array $data data.
2255 * @param array $postarr post array.
2256 *
2257 * @return mixed
2258 */
2259 public function tutor_add_gutenberg_author( $data, $postarr ) {
2260 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2261 $post_type = $postarr['post_type'];
2262 $courses_post_type = tutor()->course_post_type;
2263
2264 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2265 return $data;
2266 }
2267
2268 /**
2269 * Only admin can change main author
2270 */
2271 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2272 global $wpdb;
2273 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2274 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2275
2276 if ( $post_author > 0 ) {
2277 $data['post_author'] = $post_author;
2278 } else {
2279 $data['post_author'] = get_current_user_id();
2280 }
2281 }
2282
2283 return $data;
2284 }
2285
2286
2287 /**
2288 * Attach product with course when course save from frontend or backend.
2289 *
2290 * @since 1.3.4
2291 *
2292 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2293 *
2294 * @param integer $post_ID course ID.
2295 * @param array $post_data created course post details.
2296 *
2297 * @return void
2298 */
2299 public function attach_product_with_course( $post_ID, $post_data ) {
2300 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2301 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2302
2303 /**
2304 * For native monetization, just return
2305 * No need to attach anything.
2306 */
2307 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2308 return;
2309 }
2310
2311 /**
2312 * When course moved paid to free
2313 * Keep the product linked and return.
2314 */
2315 if ( -1 === $product_id ) {
2316 return;
2317 }
2318
2319 /**
2320 * Free user can only select product from dropdown
2321 */
2322 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2323 if ( $product_id > 0 ) {
2324 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2325 }
2326
2327 return;
2328 }
2329
2330 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2331 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2332 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2333
2334 if ( ! $course_price || $sale_price >= $course_price ) {
2335 return;
2336 }
2337
2338 $course = get_post( $post_ID );
2339
2340 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2341
2342 if ( 'wc' === $monetize_by ) {
2343
2344 $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2345
2346 if ( $is_update ) {
2347 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2348
2349 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2350 $product_obj = wc_get_product( $product_id );
2351 if ( $product_obj->is_type( 'subscription' ) ) {
2352 update_post_meta( $product_id, '_subscription_price', $course_price );
2353 }
2354
2355 // Set course regular & sale price.
2356 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2357 } else {
2358 // Create new WC product name with course title.
2359 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2360 if ( $product_id ) {
2361 $product_obj = wc_get_product( $product_id );
2362
2363 self::sync_course_with_wc_product( $post_ID, $product_id );
2364
2365 // Set course regular & sale price.
2366 self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2367 }
2368 }
2369
2370 $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2371 if ( $product_id && $course_post_thumbnail ) {
2372 set_post_thumbnail( $product_id, $course_post_thumbnail );
2373 }
2374 } elseif ( 'edd' === $monetize_by ) {
2375
2376 $is_update = false;
2377
2378 if ( $attached_product_id ) {
2379 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2380 if ( $edd_price ) {
2381 $is_update = true;
2382 }
2383 }
2384
2385 if ( $is_update ) {
2386 // Update the product.
2387 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2388 } else {
2389 // Create new product.
2390
2391 $post_arr = array(
2392 'post_type' => 'download',
2393 'post_title' => $course->post_title,
2394 'post_status' => 'publish',
2395 'post_author' => get_current_user_id(),
2396 );
2397 $download_id = wp_insert_post( $post_arr );
2398 if ( $download_id ) {
2399 // EDD edd_price.
2400 update_post_meta( $download_id, 'edd_price', $course_price );
2401
2402 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2403 // Mark product for EDD.
2404 update_post_meta( $download_id, '_tutor_product', 'yes' );
2405
2406 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2407 if ( $course_post_thumbnail ) {
2408 set_post_thumbnail( $download_id, $course_post_thumbnail );
2409 }
2410 }
2411 }
2412 }
2413 }
2414
2415 /**
2416 * Add Course level to course settings
2417 *
2418 * @since 1.4.1
2419 *
2420 * @param array $args arguments.
2421 * @return array
2422 */
2423 public function add_course_level_to_settings( $args ) {
2424 $course_id = get_the_ID();
2425 $levels = tutor_utils()->course_levels();
2426 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2427
2428 $args['general']['fields']['_tutor_course_level'] = array(
2429 'type' => 'select',
2430 'label' => __( 'Difficulty Level', 'tutor' ),
2431 'label_title' => __( 'Enable', 'tutor' ),
2432 'options' => $levels,
2433 'value' => $course_level ? $course_level : 'intermediate',
2434 'desc' => __( 'Course difficulty level', 'tutor' ),
2435 );
2436
2437 return $args;
2438 }
2439
2440 /**
2441 * Check if course starting
2442 *
2443 * @since 1.4.8
2444 * @return void
2445 */
2446 public function tutor_lesson_load_before() {
2447 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2448 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2449 if ( is_user_logged_in() ) {
2450 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2451 if ( ! $completed_lessons && ! $is_course_started ) {
2452 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2453 do_action( 'tutor/course/started', $course_id );
2454 }
2455 }
2456 }
2457
2458 /**
2459 * Add Course level to course settings
2460 *
2461 * @since 1.4.8
2462 * @return void
2463 */
2464 public function course_elements_enable_disable() {
2465 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2466 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2467 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2468 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2469 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2470 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2471 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2472 }
2473
2474 /**
2475 * Enable disable course progress bar
2476 *
2477 * @since 1.4.8
2478 *
2479 * @param string $html HTML string.
2480 * @return string
2481 */
2482 public function enable_disable_course_progress_bar( $html ) {
2483 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2484 if ( $disable_option ) {
2485 return '';
2486 }
2487 return $html;
2488 }
2489
2490 /**
2491 * Enable disable material includes
2492 *
2493 * @since 1.4.8
2494 *
2495 * @param string $html HTML string.
2496 * @return string
2497 */
2498 public function enable_disable_material_includes( $html ) {
2499 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2500 if ( $disable_option ) {
2501 return '';
2502 }
2503 return $html;
2504 }
2505
2506 /**
2507 * Enable disable course content
2508 *
2509 * @since 1.4.8
2510 *
2511 * @param string $html HTML string.
2512 * @return string
2513 */
2514 public function enable_disable_course_content( $html ) {
2515 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2516 if ( $disable_option ) {
2517 return '';
2518 }
2519 return $html;
2520 }
2521
2522 /**
2523 * Enable disable course benefits
2524 *
2525 * @since 1.4.8
2526 *
2527 * @param string $html HTML string.
2528 * @return string
2529 */
2530 public function enable_disable_course_benefits( $html ) {
2531 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2532 if ( $disable_option ) {
2533 return '';
2534 }
2535 return $html;
2536 }
2537
2538 /**
2539 * Enable disable course requirements
2540 *
2541 * @since 1.4.8
2542 *
2543 * @param string $html HTML string.
2544 * @return string
2545 */
2546 public function enable_disable_course_requirements( $html ) {
2547 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2548 if ( $disable_option ) {
2549 return '';
2550 }
2551 return $html;
2552 }
2553
2554 /**
2555 * Enable disable course target audience
2556 *
2557 * @since 1.4.8
2558 *
2559 * @param string $html HTML string.
2560 * @return string
2561 */
2562 public function enable_disable_course_target_audience( $html ) {
2563 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2564 if ( $disable_option ) {
2565 return '';
2566 }
2567 return $html;
2568 }
2569
2570 /**
2571 * Enable disable course nav items
2572 *
2573 * @since 1.4.8
2574 *
2575 * @param array $items item list.
2576 * @param integer $course_id course ID.
2577 *
2578 * @return array
2579 */
2580 public function enable_disable_course_nav_items( $items, $course_id ) {
2581 global $wp_query, $post;
2582 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2583 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2584 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2585
2586 // Whether Q&A enabled.
2587 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2588 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2589 unset( $items['questions'] );
2590 }
2591 }
2592
2593 // Whether announcment enabled.
2594 if ( $disable_course_announcements ) {
2595 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2596 unset( $items['announcements'] );
2597 }
2598 }
2599
2600 // Hide review section if disabled.
2601 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2602 unset( $items['reviews'] );
2603 }
2604
2605 // Whether enrollment require.
2606 $is_enrolled = tutor_utils()->is_enrolled();
2607
2608 return array_filter(
2609 $items,
2610 function ( $item ) use ( $is_enrolled ) {
2611 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2612 return $is_enrolled;
2613 }
2614 return true;
2615 }
2616 );
2617 }
2618
2619 /**
2620 * Filter product in shop page
2621 *
2622 * @since 1.4.9
2623 * @return void|null
2624 */
2625 public function filter_product_in_shop_page() {
2626 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2627 if ( ! $hide_course_from_shop_page ) {
2628 return;
2629 }
2630 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2631 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2632 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2633 }
2634
2635
2636 /**
2637 * Tutor product meta query
2638 *
2639 * @since 1.4.9
2640 * @return array
2641 */
2642 public function tutor_product_meta_query() {
2643 $meta_query = array(
2644 'key' => '_tutor_product',
2645 'compare' => 'NOT EXISTS',
2646 );
2647 return $meta_query;
2648 }
2649
2650 /**
2651 * Filter product in woocommerce shop page
2652 *
2653 * @since 1.4.9
2654 *
2655 * @param \WP_Query $wp_query WP Query instance.
2656 * @return \WP_Query
2657 */
2658 public function filter_woocommerce_product_query( $wp_query ) {
2659 $product_ids = $this->get_connected_wc_product_ids();
2660 $wp_query->set( 'post__not_in', $product_ids );
2661 return $wp_query;
2662 }
2663
2664 /**
2665 * Get connected woocommerce product ids for course and course bundle
2666 *
2667 * @since 2.7.2
2668 *
2669 * @return array
2670 */
2671 public function get_connected_wc_product_ids() {
2672 global $wpdb;
2673
2674 $results = $wpdb->get_results(
2675 $wpdb->prepare(
2676 "SELECT DISTINCT pm.meta_value product_id
2677 FROM {$wpdb->posts} p
2678 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2679 AND pm.meta_key = %s
2680 WHERE post_type IN( 'courses','course-bundle' )",
2681 '_tutor_course_product_id'
2682 )
2683 );
2684
2685 $ids = array();
2686 if ( is_array( $results ) && count( $results ) ) {
2687 $ids = array_column( $results, 'product_id' );
2688 }
2689
2690 return $ids;
2691 }
2692
2693 /**
2694 * Filter product in edd downloads shortcode page
2695 *
2696 * @since 1.4.9
2697 *
2698 * @param \WP_Query $query WP Query instance.
2699 * @return \WP_Query
2700 */
2701 public function filter_edd_downloads_query( $query ) {
2702 $query['meta_query'][] = $this->tutor_product_meta_query();
2703 return $query;
2704 }
2705
2706 /**
2707 * Filter product in edd downloads archive page
2708 *
2709 * @since 1.4.9
2710 *
2711 * @param \WP_Query $wp_query WP Query instance.
2712 * @return \WP_Query
2713 */
2714 public function filter_archive_meta_query( $wp_query ) {
2715 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2716 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2717 }
2718 return $wp_query;
2719 }
2720
2721 /**
2722 * Removed course price if already enrolled at single course
2723 *
2724 * @since 1.5.8
2725 *
2726 * @param string $html HTML string.
2727 * @return string
2728 */
2729 public function remove_price_if_enrolled( $html ) {
2730 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2731
2732 if ( $should_removed ) {
2733 $course_id = get_the_ID();
2734 $enrolled = tutor_utils()->is_enrolled( $course_id );
2735 if ( $enrolled ) {
2736 $html = '';
2737 }
2738 }
2739 return $html;
2740 }
2741
2742 /**
2743 * Check if all lessons and quizzes done before mark course complete.
2744 *
2745 * @since 1.5.8
2746 *
2747 * @param string $html HTML string.
2748 * @return string
2749 */
2750 public function tutor_lms_hide_course_complete_btn( $html ) {
2751
2752 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2753 if ( 'strict' !== $completion_mode ) {
2754 return $html;
2755 }
2756
2757 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2758 $lesson_count = tutor_utils()->get_lesson_count_by_course();
2759
2760 if ( $completed_lesson < $lesson_count ) {
2761 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2762 <div class="tutor-alert-text">
2763 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2764 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2765 </div>
2766 </div>';
2767 }
2768
2769 $quizzes = array();
2770 $assignments = array();
2771
2772 $course_contents = tutor_utils()->get_course_contents_by_id();
2773 if ( tutor_utils()->count( $course_contents ) ) {
2774 foreach ( $course_contents as $content ) {
2775 if ( 'tutor_quiz' === $content->post_type ) {
2776 $quizzes[] = $content;
2777 }
2778 if ( 'tutor_assignments' === $content->post_type ) {
2779 $assignments[] = $content;
2780 }
2781 }
2782 }
2783
2784 $required_assignment_pass = 0;
2785
2786 foreach ( $assignments as $row ) {
2787
2788 $submitted_assignment = tutor_utils()->is_assignment_submitted( $row->ID );
2789 $is_reviewed_by_instructor = null === $submitted_assignment
2790 ? false
2791 : get_comment_meta( $submitted_assignment->comment_ID, 'evaluate_time', true );
2792
2793 if ( $submitted_assignment && $is_reviewed_by_instructor ) {
2794 $pass_mark = tutor_utils()->get_assignment_option( $submitted_assignment->comment_post_ID, 'pass_mark' );
2795 $given_mark = get_comment_meta( $submitted_assignment->comment_ID, 'assignment_mark', true );
2796
2797 if ( $given_mark < $pass_mark ) {
2798 $required_assignment_pass++;
2799 }
2800 } else {
2801 $required_assignment_pass++;
2802 }
2803 }
2804
2805 $is_quiz_pass = true;
2806 $required_quiz_pass = 0;
2807
2808 if ( tutor_utils()->count( $quizzes ) ) {
2809 foreach ( $quizzes as $quiz ) {
2810
2811 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2812 if ( $attempt ) {
2813 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2814 $earned_percentage = QuizModel::calculate_attempt_earned_percentage( $attempt );
2815
2816 if ( $earned_percentage < $passing_grade ) {
2817 $required_quiz_pass++;
2818 $is_quiz_pass = false;
2819 }
2820 } else {
2821 $required_quiz_pass++;
2822 $is_quiz_pass = false;
2823 }
2824 }
2825 }
2826
2827 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2828 $_msg = '';
2829 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2830 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2831
2832 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2833 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2834 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2835 }
2836
2837 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2838 //phpcs:ignore
2839 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2840 }
2841
2842 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2843 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2844 $_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 );
2845 }
2846
2847 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2848 <div class="tutor-alert-text">
2849 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2850 <span>' . $_msg . '</span>
2851 </div>
2852 </div>';
2853 }
2854
2855 return $html;
2856 }
2857
2858 /**
2859 * Generate Gradebook
2860 *
2861 * @since 1.5.8
2862 *
2863 * @param string $html HTML string.
2864 * @return string
2865 */
2866 public function get_generate_greadbook( $html ) {
2867 if ( ! tutor_utils()->is_completed_course() ) {
2868 return '';
2869 }
2870 return $html;
2871 }
2872
2873 /**
2874 * Add social share content in header
2875 *
2876 * @since 1.6.3
2877 * @return void
2878 */
2879 public function social_share_content() {
2880 global $wp_query, $post;
2881 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2882 <!--Facebook-->
2883 <meta property="og:type" content="website"/>
2884 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2885 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2886 <!--Twitter-->
2887 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2888 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2889 <!--Google+-->
2890 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2891 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2892 <?php
2893 }
2894 }
2895
2896 /**
2897 * Delete associated enrollment
2898 *
2899 * @since 1.8.2
2900 *
2901 * @param integer $post_id post ID.
2902 * @return void
2903 */
2904 public function delete_associated_enrollment( $post_id ) {
2905 global $wpdb;
2906
2907 $enroll_id = $wpdb->get_var(
2908 $wpdb->prepare(
2909 "SELECT
2910 post_id
2911 FROM
2912 {$wpdb->postmeta}
2913 WHERE
2914 meta_key='_tutor_enrolled_by_order_id'
2915 AND meta_value = %d
2916 ",
2917 $post_id
2918 )
2919 );
2920
2921 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2922
2923 $course_id = get_post_field( 'post_parent', $enroll_id );
2924 $user_id = get_post_field( 'post_author', $enroll_id );
2925
2926 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2927 }
2928 }
2929
2930 /**
2931 * Reset course progress.
2932 *
2933 * @since 1.5.8
2934 * @return void
2935 */
2936 public function tutor_reset_course_progress() {
2937 tutor_utils()->checking_nonce();
2938 $course_id = Input::post( 'course_id' );
2939
2940 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2941 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2942 return;
2943 }
2944
2945 tutor_utils()->delete_course_progress( $course_id );
2946 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2947 }
2948
2949 /**
2950 * Do enroll if guest attempt to enroll and course is free
2951 *
2952 * @since 1.9.8
2953 *
2954 * @param integer $course_id course ID.
2955 * @param integer $user_id user ID.
2956
2957 * @return void
2958 */
2959 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
2960 $course_id = sanitize_text_field( $course_id );
2961 $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
2962
2963 if ( $course_id && $is_allowed ) {
2964 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2965 if ( ! $is_purchasable ) {
2966 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
2967 do_action( 'guest_attempt_after_enrollment', $course_id );
2968 }
2969 }
2970 }
2971
2972 /**
2973 * Handle course enrollment
2974 *
2975 * @since 2.1.0
2976 * @return void
2977 */
2978 public function course_enrollment() {
2979 tutor_utils()->checking_nonce();
2980
2981 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2982 $user_id = get_current_user_id();
2983
2984 if ( $course_id ) {
2985 $password_protected = post_password_required( $course_id );
2986 if ( $password_protected ) {
2987 wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
2988 }
2989 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
2990 if ( $enroll ) {
2991 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
2992 } else {
2993 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
2994 }
2995 } else {
2996 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
2997 }
2998 }
2999
3000 /**
3001 * After trash a course direct to the course list page
3002 *
3003 * @since 2.1.7
3004 *
3005 * @param integer $post_id int course id.
3006 *
3007 * @return void
3008 */
3009 public static function redirect_to_course_list_page( int $post_id ): void {
3010 $post = get_post( $post_id );
3011 if ( tutor()->course_post_type === $post->post_type ) {
3012 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
3013 if ( ! $is_gutenberg_enabled ) {
3014 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
3015 exit;
3016 }
3017 }
3018 }
3019
3020 /**
3021 * Create or update WooCommerce product
3022 *
3023 * If product id not set it will create new one.
3024 *
3025 * @since 2.2.0
3026 *
3027 * @param string $title product title.
3028 * @param string $reg_price product price.
3029 * @param string $sale_price product sale price.
3030 * @param int $product_id product ID.
3031 * @param string $status product status.
3032 *
3033 * @return integer Product id or return 0 if WC not exists
3034 */
3035 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
3036 if ( ! tutor_utils()->has_wc() ) {
3037 return 0;
3038 }
3039
3040 $product_obj = new \WC_Product();
3041 if ( $product_id ) {
3042 $product_obj = wc_get_product( $product_id );
3043 }
3044
3045 $product_obj->set_name( $title );
3046 $product_obj->set_status( $status );
3047 $product_obj->set_price( $reg_price );
3048 $product_obj->set_regular_price( $reg_price );
3049
3050 if ( $sale_price > 0 ) {
3051 $product_obj->set_sale_price( $sale_price );
3052 } else {
3053 $product_obj->set_sale_price( null );
3054 }
3055
3056 $product_obj->set_sold_individually( true );
3057
3058 return $product_obj->save();
3059 }
3060
3061 /**
3062 * Get course/bundle mini info
3063 *
3064 * @since 3.0.0
3065 *
3066 * @param object $post Course or bundle post.
3067 *
3068 * @return array
3069 */
3070 public static function get_mini_info( object $post ) {
3071 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3072 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3073 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3074 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
3075
3076 $info = array(
3077 'id' => $post->ID,
3078 'title' => $post->post_title,
3079 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3080 'is_purchasable' => $is_purchasable,
3081 'regular_price' => $regular_price,
3082 'sale_price' => $sale_price,
3083 );
3084
3085 $card_data = apply_filters( 'tutor_course_mini_info', $info, $post );
3086
3087 return $card_data;
3088 }
3089
3090 /**
3091 * Get course/bundle card data
3092 *
3093 * This method will return all data that contain in
3094 * course card
3095 *
3096 * @since 3.0.0
3097 *
3098 * @param object $post Course or bundle post.
3099 *
3100 * @return array
3101 */
3102 public static function get_card_data( object $post ) {
3103 $info = self::get_mini_info( $post );
3104
3105 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3106 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3107 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3108
3109 $card_data = apply_filters( 'tutor_course_card_data', $info, $post );
3110
3111 return $card_data;
3112 }
3113
3114 /**
3115 * Filter user list access for instructor
3116 *
3117 * @since 3.0.0
3118 *
3119 * @param bool $access access.
3120 *
3121 * @return bool
3122 */
3123 public function user_list_access_for_instructor( $access ) {
3124 $is_instructor = User::is_instructor();
3125 return $access || $is_instructor;
3126 }
3127
3128 /**
3129 * Filter user list args for instructor
3130 *
3131 * @since 3.0.0
3132 *
3133 * @param array $args args.
3134 *
3135 * @return array
3136 */
3137 public function user_list_args_for_instructor( $args ) {
3138 if ( User::is_instructor() ) {
3139 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3140 unset( $args['fields']['user_email'] );
3141 }
3142 }
3143
3144 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3145 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3146 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3147 }
3148
3149 return $args;
3150 }
3151
3152 /**
3153 * Get a list of possible course status.
3154 *
3155 * @since 3.6.2
3156 *
3157 * @return array
3158 */
3159 public static function course_status_list() {
3160 return array(
3161 'publish',
3162 'private',
3163 'draft',
3164 'trash',
3165 'pending',
3166 'future',
3167 );
3168 }
3169
3170 /**
3171 * Link a course/bundle post to a WooCommerce product.
3172 *
3173 * @since 3.8.2
3174 *
3175 * @param int $post_ID The WordPress post ID of the course.
3176 * @param int $product_id The WooCommerce product ID to associate with the course.
3177 * @return void
3178 */
3179 public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3180
3181 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
3182
3183 // Mark product for woocommerce.
3184 update_post_meta( $product_id, '_virtual', 'yes' );
3185 update_post_meta( $product_id, '_tutor_product', 'yes' );
3186 }
3187
3188 /**
3189 * Map Tutor's course prices to WooCommerce.
3190 *
3191 * @since 3.8.2
3192 *
3193 * @param int $post_ID The WordPress post ID of the course.
3194 * @param string|int|float $regular_price The regular price.
3195 * @param string|int|float $sale_price The sale price.
3196 * @return void
3197 */
3198 private static function set_course_regular_and_sale_price( $post_ID, $regular_price, $sale_price ) {
3199
3200 // Set course regular & sale price.
3201 update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3202 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3203 }
3204 }
3205