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