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