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