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