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