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