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