PluginProbe
Tutor LMS – eLearning and online course solution / 3.9.15
Tutor LMS – eLearning and online course solution v3.9.15
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 +3067 -314 1.0.63.9.15 View file →
@@ -1,523 +1,3276 @@
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\Ecommerce\Tax;
20 +use Tutor\Models\QuizModel;
21 +use Tutor\Helpers\HttpHelper;
22 +use Tutor\Models\CourseModel;
23 +use Tutor\Ecommerce\Ecommerce;
24 +use Tutor\Traits\JsonResponse;
25 +use Tutor\Helpers\ValidationHelper;
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'), 10, 2);
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, 'ajax_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, $post){
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)){
125 - $i = 0;
126 - foreach ($order as $topic ){
127 - $i++;
128 - $wpdb->update(
129 - $wpdb->posts,
130 - array('menu_order' => $i),
131 - array('ID' => $topic['topic_id'])
132 - );
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 + }
133 1129
134 - /**
135 - * Removing All lesson with topic
136 - */
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 + }
137 1134
138 - $wpdb->update(
139 - $wpdb->posts,
140 - array('post_parent' => 0),
141 - array('post_parent' => $topic['topic_id'])
142 - );
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 + }
143 1140
144 - /**
145 - * Lesson Attaching with topic ID
146 - * sorting lesson
147 - */
148 - if (isset($topic['lesson_ids'])){
149 - $lesson_ids = $topic['lesson_ids'];
150 - }else{
151 - $lesson_ids = array();
152 - }
153 - if (count($lesson_ids)){
154 - foreach ($lesson_ids as $lesson_key => $lesson_id ){
155 - $wpdb->update(
156 - $wpdb->posts,
157 - array('post_parent' => $topic['topic_id'], 'menu_order' => $lesson_key),
158 - array('ID' => $lesson_id)
159 - );
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 + } elseif ( 'divi' === $builder ) {
1178 + $old_post_content = get_post_meta( $course_id, '_et_pb_old_content', true );
1179 + $course = get_post( $course_id );
1180 + $course->post_content = $old_post_content;
1181 + $result = wp_update_post( $course );
1182 +
1183 + if ( $result && ! is_wp_error( $result ) ) {
1184 + update_post_meta( $course_id, '_et_pb_use_builder', 'off' );
1185 + update_post_meta( $course_id, '_et_pb_old_content', '' );
1186 + delete_post_meta( $course_id, '_et_dynamic_cached_shortcodes' );
1187 + delete_post_meta( $course_id, '_et_dynamic_cached_attributes' );
1188 + delete_post_meta( $course_id, '_et_builder_module_features_cache' );
1189 + }
1190 + }
1191 +
1192 + $this->json_response(
1193 + __( 'Builder unlinked successfully.', 'tutor' ),
1194 + $course_id,
1195 + HttpHelper::STATUS_OK
1196 + );
1197 + }
1198 +
1199 + /**
1200 + * Get all course contents by course id.
1201 + *
1202 + * @since 3.0.0
1203 + *
1204 + * @param int $course_id course id.
1205 + *
1206 + * @return array
1207 + */
1208 + public function get_course_contents( $course_id ) {
1209 + $data = array();
1210 + $topics = tutor_utils()->get_topics( $course_id );
1211 +
1212 + if ( $topics->have_posts() ) {
1213 + foreach ( $topics->get_posts() as $post ) {
1214 + $current_topic = array(
1215 + 'id' => $post->ID,
1216 + 'title' => $post->post_title,
1217 + 'summary' => $post->post_content,
1218 + 'contents' => array(),
1219 + );
1220 +
1221 + $topic_contents = tutor_utils()->get_course_contents_by_topic( $post->ID, -1 );
1222 +
1223 + if ( $topic_contents->have_posts() ) {
1224 + foreach ( $topic_contents->get_posts() as $post ) {
1225 + if ( tutor()->quiz_post_type === $post->post_type ) {
1226 + $questions = tutor_utils()->get_questions_by_quiz( $post->ID );
1227 + $post->total_question = is_array( $questions ) ? count( $questions ) : 0;
160 1228 }
1229 +
1230 + array_push( $current_topic['contents'], $post );
161 1231 }
162 1232 }
1233 +
1234 + $current_topic = apply_filters( 'tutor_filter_course_content', $current_topic );
1235 +
1236 + array_push( $data, $current_topic );
163 1237 }
164 1238 }
165 1239
1240 + return $data;
1241 + }
166 1242
167 - //Video
168 - if ( ! empty($_POST['video']['source'])){
169 - $video = tutor_utils()->sanitize_array($_POST['video']);
170 - update_post_meta($post_ID, '_video', $video);
1243 + /**
1244 + * Get course contents
1245 + *
1246 + * @since 3.0.0
1247 + */
1248 + public function ajax_course_contents() {
1249 + tutor_utils()->check_nonce();
1250 +
1251 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1252 +
1253 + $this->check_access( $course_id );
1254 +
1255 + if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1256 + $errors['course_id'] = __( 'Invalid course id', 'tutor' );
171 1257 }
172 1258
1259 + if ( ! empty( $errors ) ) {
1260 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1261 + }
1262 +
1263 + $contents = $this->get_course_contents( $course_id );
1264 +
1265 + $this->json_response(
1266 + __( 'Course contents fetched successfully', 'tutor' ),
1267 + $contents
1268 + );
1269 + }
1270 +
1271 + /**
1272 + * Get course details by ID
1273 + *
1274 + * @since 3.0.0
1275 + *
1276 + * @return void
1277 + */
1278 + public function ajax_course_details() {
1279 + tutor_utils()->check_nonce();
1280 +
1281 + $errors = array();
1282 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1283 +
1284 + $this->check_access( $course_id );
1285 +
1286 + if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1287 + $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1288 + }
1289 +
1290 + if ( ! empty( $errors ) ) {
1291 + $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1292 + }
1293 +
1294 + $price_type = tutor_utils()->price_type( $course_id );
1295 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1296 +
1297 + $product_name = '';
1298 + $price = 0;
1299 + $sale_price = 0;
1300 + $product_id = tutor_utils()->get_course_product_id( $course_id );
1301 +
1302 + if ( 'wc' === $monetize_by ) {
1303 + $product = wc_get_product( $product_id );
1304 + if ( $product ) {
1305 + $product_name = $product->get_name();
1306 + $price = $product->get_regular_price();
1307 + $sale_price = $product->get_sale_price();
1308 + }
1309 + }
1310 +
1311 + if ( 'tutor' === $monetize_by ) {
1312 + $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1313 + $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1314 + }
1315 +
1316 + $course_pricing = array(
1317 + 'type' => $price_type,
1318 + 'product_id' => $product_id,
1319 + 'product_name' => $product_name,
1320 + 'price' => $price,
1321 + 'sale_price' => $sale_price,
1322 + );
1323 +
1324 + $video_intro = get_post_meta( $course_id, '_video', true );
1325 + if ( $video_intro ) {
1326 + $source = $video_intro['source'] ?? '';
1327 + if ( 'html5' === $source ) {
1328 + $poster_url = wp_get_attachment_url( $video['poster'] ?? 0 );
1329 + $source_html5 = wp_get_attachment_url( $video['source_video_id'] ?? 0 );
1330 + $video['poster_url'] = $poster_url;
1331 + $video['source_html5'] = $source_html5;
1332 + }
1333 + }
1334 +
1335 + $course = get_post( $course_id, ARRAY_A );
1336 + if ( $course ) {
1337 + $course['post_name'] = urldecode( $course['post_name'] );
1338 + }
1339 +
1340 + $editors = tutor_utils()->get_editor_list( $course_id );
1341 +
1342 + $data = array(
1343 + 'editors' => array_values( $editors ),
1344 + 'editor_used' => tutor_utils()->get_editor_used( $course_id ),
1345 + 'preview_link' => get_preview_post_link( $course_id ),
1346 + 'post_author' => tutor_utils()->get_tutor_user( $course['post_author'] ),
1347 + 'course_categories' => wp_get_post_terms( $course_id, CourseModel::COURSE_CATEGORY ),
1348 + 'course_tags' => wp_get_post_terms( $course_id, CourseModel::COURSE_TAG ),
1349 + 'thumbnail_id' => get_post_meta( $course_id, '_thumbnail_id', true ),
1350 + 'thumbnail' => get_the_post_thumbnail_url( $course_id ),
1351 +
1352 + 'enable_qna' => get_post_meta( $course_id, '_tutor_enable_qa', true ),
1353 + 'is_public_course' => get_post_meta( $course_id, '_tutor_is_public_course', true ),
1354 + 'course_level' => get_post_meta( $course_id, '_tutor_course_level', true ),
1355 + 'video' => $video_intro,
1356 + 'course_duration' => get_post_meta( $course_id, '_course_duration', true ),
1357 + 'course_benefits' => get_post_meta( $course_id, '_tutor_course_benefits', true ),
1358 + 'course_requirements' => get_post_meta( $course_id, '_tutor_course_requirements', true ),
1359 + 'course_target_audience' => get_post_meta( $course_id, '_tutor_course_target_audience', true ),
1360 + 'course_material_includes' => get_post_meta( $course_id, '_tutor_course_material_includes', true ),
1361 + 'monetize_by' => $monetize_by,
1362 + 'course_pricing' => $course_pricing,
1363 + 'course_settings' => get_post_meta( $course_id, '_tutor_course_settings', true ),
1364 + 'step_completion_status' => array(
1365 + 'basic' => true,
1366 + 'curriculum' => false,
1367 + 'additional' => false,
1368 + 'certificate' => false,
1369 + ),
1370 + );
1371 +
1372 + $tax_on_single = get_post_meta( $course_id, self::TAX_ON_SINGLE_META, true );
1373 + $tax_on_subscription = get_post_meta( $course_id, self::TAX_ON_SUBSCRIPTION_META, true );
1374 +
1375 + $data['tax_collection'] = array(
1376 + 'tax_on_single' => '' === $tax_on_single ? '1' : $tax_on_single,
1377 + 'tax_on_subscription' => '' === $tax_on_subscription ? '1' : $tax_on_subscription,
1378 + );
1379 +
1380 + $data = apply_filters( 'tutor_course_details_response', array_merge( $course, $data ) );
1381 +
1382 + $this->json_response( __( 'Data retrieved successfully!', 'tutor' ), $data );
1383 + }
1384 +
1385 + /**
1386 + * Load course builder.
1387 + *
1388 + * @since 3.0.0
1389 + *
1390 + * @return void
1391 + */
1392 + public function load_course_builder() {
1393 + global $pagenow;
1394 +
1395 + $has_pro = tutor()->has_pro;
1396 + $has_access_role = User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) );
1397 +
1398 + $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1399 + $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1400 + $backend_edit = $backend_builder && $course_id;
1401 +
1402 + $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1403 + $frontend_edit = $is_frontend_builder && $course_id;
1404 +
1405 + if ( $has_access_role && ( $backend_edit || ( $has_pro && $frontend_edit ) ) ) {
1406 + $post_type = get_post_type( $course_id );
1407 + $can_edit_course = tutor_utils()->can_user_edit_course( get_current_user_id(), $course_id );
1408 +
1409 + if ( tutor()->course_post_type === $post_type && ( User::is_admin() || $can_edit_course ) ) {
1410 + /**
1411 + * Edit trash course behavior
1412 + *
1413 + * @since 3.0.0
1414 + */
1415 + if ( CourseModel::STATUS_TRASH === get_post_status( $course_id ) ) {
1416 + $message = User::is_admin()
1417 + ? __( 'You cannot edit this course because it is in the Trash. Please restore it and try again', 'tutor' )
1418 + : tutor_utils()->error_message();
1419 + wp_die( esc_html( $message ) );
1420 + }
1421 +
1422 + $this->load_course_builder_view();
1423 + }
1424 + }
1425 + }
1426 +
1427 + /**
1428 + * Enqueue course builder assets like CSS, JS
1429 + *
1430 + * @since 3.0.0
1431 + *
1432 + * @return void
1433 + */
1434 + public function enqueue_course_builder_assets() {
1435 + // Fix: function print_emoji_styles is deprecated since version 6.4.0!
1436 + remove_action( 'wp_print_styles', 'print_emoji_styles' );
1437 + remove_action( 'wp_head', 'wp_admin_bar_header' );
1438 + add_action( 'wp_head', 'wp_enqueue_admin_bar_header_styles' );
1439 +
1440 + do_action( 'tutor_course_builder_before_wp_editor_load' );
1441 + wp_enqueue_script( 'wp-tinymce' );
1442 + wp_enqueue_script( 'mce-view' );
1443 + wp_enqueue_editor();
1444 +
1445 + wp_enqueue_media();
1446 + 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 );
1447 + wp_set_script_translations( 'tutor-course-builder', 'tutor', tutor()->path . 'languages/' );
1448 +
1449 + wp_localize_script(
1450 + 'mce-view',
1451 + 'mceViewL10n',
1452 + array(
1453 + 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1454 + )
1455 + );
1456 + }
1457 +
1458 + /**
1459 + * Localize custom course builder data for _tutorobject.
1460 + *
1461 + * @since 3.3.1
1462 + *
1463 + * @param array $data the localized data.
1464 + *
1465 + * @return array
1466 + */
1467 + public function localize_course_builder_data( $data ) {
1468 + global $pagenow;
1469 +
1470 + $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1471 + $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1472 + $backend_edit = $backend_builder && $course_id;
1473 +
1474 + $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1475 + $frontend_edit = $is_frontend_builder && $course_id;
1476 +
1477 + if ( ! $backend_edit && ! $frontend_edit ) {
1478 + return $data;
1479 + }
1480 +
173 1481 /**
174 - * Adding author to instructor automatically
1482 + * Prepare course builder data.
175 1483 */
1484 + $default_data = ( new Assets( false ) )->get_default_localized_data();
176 1485
177 - $author_id = $post->post_author;
178 - $attached = (int) $wpdb->get_var(" SELECT COUNT(umeta_id) FROM {$wpdb->usermeta} WHERE user_id = {$author_id} AND meta_key = '_tutor_instructor_course_id' AND meta_value = {$post_ID} ");
179 - if ( ! $attached){
180 - add_user_meta($author_id, '_tutor_instructor_course_id', $post_ID);
1486 + if ( isset( $default_data['current_user']['data']['id'] ) ) {
1487 + $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1488 + $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
181 1489 }
182 1490
183 - //Announcements
184 - $announcement_title = tutor_utils()->avalue_dot('announcements.title', $_POST );
185 - if ( ! empty($announcement_title)){
186 - $title = sanitize_text_field(tutor_utils()->avalue_dot('announcements.title', $_POST ));
187 - $content = wp_kses_post(tutor_utils()->avalue_dot('announcements.content', $_POST ));
1491 + /**
1492 + * Localized only options to protect sensitive info like API keys.
1493 + */
1494 + $required_options = array(
1495 + 'monetize_by',
1496 + 'enable_course_marketplace',
1497 + 'course_permalink_base',
1498 + 'supported_video_sources',
1499 + 'enrollment_expiry_enabled',
1500 + 'enable_q_and_a_on_course',
1501 + 'instructor_can_delete_course',
1502 + 'chatgpt_enable',
1503 + 'hide_admin_bar_for_users',
1504 + 'enable_redirect_on_course_publish_from_frontend',
1505 + 'instructor_can_publish_course',
1506 + 'instructor_can_change_course_author',
1507 + 'instructor_can_manage_co_instructors',
1508 + );
188 1509
189 - $post_arr = array(
190 - 'post_type' => 'tutor_announcements',
191 - 'post_title' => $title,
192 - 'post_content' => $content,
193 - 'post_status' => 'publish',
194 - 'post_author' => get_current_user_id(),
195 - 'post_parent' => $post_ID,
1510 + $full_settings = get_option( 'tutor_option', array() );
1511 + $settings = Options_V2::get_only( $required_options );
1512 + $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1513 + $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1514 + $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1515 +
1516 + $settings['enable_tax'] = Tax::get_setting( 'enable_tax', true );
1517 + $settings['is_tax_included_in_price'] = Tax::is_tax_included_in_price();
1518 + $settings['enable_individual_tax_control'] = Tax::get_setting( 'enable_individual_tax_control' );
1519 +
1520 + $new_data = array( 'settings' => $settings );
1521 +
1522 + $data = array_merge( $default_data, $new_data );
1523 +
1524 + /**
1525 + * Course builder dashboard URL based on role and settings.
1526 + */
1527 + $dashboard_url = tutor_utils()->tutor_dashboard_url();
1528 + if ( User::is_admin() ) {
1529 + $dashboard_url = get_admin_url();
1530 + }
1531 +
1532 + /**
1533 + * EDD product list
1534 + */
1535 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1536 + if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1537 + $data['edd_products'] = tutor_utils()->get_edd_products();
1538 + }
1539 +
1540 + $difficulty_levels = array();
1541 + foreach ( tutor_utils()->course_levels() as $value => $label ) {
1542 + $difficulty_levels[] = array(
1543 + 'label' => $label,
1544 + 'value' => $value,
196 1545 );
197 - wp_insert_post( $post_arr );
198 1546 }
1547 +
1548 + $supported_video_sources = array();
1549 + $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1550 +
1551 + foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1552 + if ( in_array( $value, $saved_video_source_list, true ) ) {
1553 + $supported_video_sources[] = array(
1554 + 'label' => $label,
1555 + 'value' => $value,
1556 + );
1557 + }
1558 + }
1559 +
1560 + $data['dashboard_url'] = $dashboard_url;
1561 + $data['backend_course_list_url'] = get_admin_url( null, 'admin.php?page=tutor' );
1562 + $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1563 + $data['timezones'] = tutor_global_timezone_lists();
1564 + $data['difficulty_levels'] = $difficulty_levels;
1565 + $data['supported_video_sources'] = $supported_video_sources;
1566 + $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1567 +
1568 + if ( 'en_US' !== $data['local'] ) {
1569 + $data['course_builder_basic_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-basic', $data['local'] );
1570 + $data['course_builder_curriculum_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-curriculum', $data['local'] );
1571 + $data['course_builder_additional_locales'] = tutils()->get_script_locale_data( 'tutor-course-builder-additional', $data['local'] );
1572 + }
1573 +
1574 + $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1575 +
1576 + return $data;
199 1577 }
200 1578
201 1579 /**
202 - * Tutor add course topic
1580 + * Load view for course builder.
1581 + *
1582 + * @since 3.0.0
1583 + *
1584 + * @return void
203 1585 */
204 - public function tutor_add_course_topic(){
205 - if (empty($_POST['topic_title'])) {
206 - wp_send_json_error();
1586 + public function load_course_builder_view() {
1587 + /**
1588 + * Hide admin menu and footer.
1589 + *
1590 + * @since 3.3.0
1591 + */
1592 + echo '<style>
1593 + #adminmenumain, #wpfooter, .notice, #tutor-page-wrap { display: none !important; }
1594 + #wpcontent { margin: 0 !important; padding: 0 !important; }
1595 + #wpbody-content { padding-bottom: 0px !important; float: none; }
1596 + </style>';
1597 +
1598 + do_action( 'tutor_before_course_builder_load' );
1599 + include_once tutor()->path . 'views/pages/course-builder.php';
1600 + do_action( 'tutor_after_course_builder_load' );
1601 + }
1602 +
1603 + /**
1604 + * Add enroll require login class
1605 + *
1606 + * @since 2.6.0
1607 + *
1608 + * @param string $class_name css class name.
1609 + *
1610 + * @return string
1611 + */
1612 + public function add_enroll_required_login_class( $class_name ) {
1613 + $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1614 + if ( ! $enabled_tutor_login ) {
1615 + return '';
207 1616 }
208 - $course_id = (int) tutor_utils()->avalue_dot('tutor_topic_course_ID', $_POST);
209 - $next_topic_order_id = tutor_utils()->get_next_topic_order_id($course_id);
210 1617
211 - $topic_title = sanitize_text_field( $_POST['topic_title'] );
212 - $topic_summery = wp_kses_post( $_POST['topic_summery'] );
1618 + return $class_name;
1619 + }
213 1620
214 - $post_arr = array(
215 - 'post_type' => 'topics',
216 - 'post_title' => $topic_title,
217 - 'post_content' => $topic_summery,
218 - 'post_status' => 'publish',
219 - 'post_author' => get_current_user_id(),
220 - 'post_parent' => $course_id,
221 - 'menu_order' => $next_topic_order_id,
1621 + /**
1622 + * Get list of WC products.
1623 + *
1624 + * @since 2.5.0
1625 + * @since 3.0.0 exclude_linked_products, course_id are added.
1626 + *
1627 + * @return void
1628 + */
1629 + public function get_wc_products() {
1630 + $exclude = array();
1631 + $exclude_linked_products = Input::has( 'exclude_linked_products' );
1632 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1633 +
1634 + if ( $exclude_linked_products ) {
1635 + $exclude = tutor_utils()->get_linked_product_ids();
1636 + }
1637 +
1638 + if ( $course_id ) {
1639 + $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1640 + if ( $linked_product_id ) {
1641 + $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1642 + }
1643 + }
1644 +
1645 + $exclude = array_unique( $exclude );
1646 +
1647 + $this->json_response(
1648 + __( 'Products retrieved successfully!', 'tutor' ),
1649 + tutor_utils()->get_wc_products_db( $exclude ),
1650 + HttpHelper::STATUS_OK
222 1651 );
223 - $current_topic_id = wp_insert_post( $post_arr );
1652 + }
224 1653
225 - ob_start();
226 - include tutor()->path.'views/metabox/course-contents.php';
227 - $course_contents = ob_get_clean();
1654 + /**
1655 + * Get course associate WC product info by Ajax request
1656 + *
1657 + * @since 2.0.7
1658 + *
1659 + * @return void
1660 + */
1661 + public function get_wc_product() {
1662 + tutor_utils()->checking_nonce();
1663 + $product_id = Input::post( 'product_id' );
1664 + $product = wc_get_product( $product_id );
1665 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
228 1666
229 - wp_send_json_success(array('course_contents' => $course_contents));
1667 + $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1668 +
1669 + /**
1670 + * If selected product is already linked with
1671 + * a course & it is not the current course the
1672 + * return error
1673 + *
1674 + * @since 2.1.0
1675 + */
1676 + if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1677 + wp_send_json_error(
1678 + __( 'One product can not be added to multiple course!', 'tutor' )
1679 + );
1680 + }
1681 +
1682 + if ( $product ) {
1683 + $data = array(
1684 + 'name' => $product->get_name(),
1685 + 'regular_price' => $product->get_regular_price(),
1686 + 'sale_price' => $product->get_sale_price(),
1687 + );
1688 + wp_send_json_success( $data );
1689 + } else {
1690 + wp_send_json_error( __( 'Product not found', 'tutor' ) );
1691 + }
230 1692 }
231 1693
232 1694 /**
233 - * Update the topic
1695 + * Update course content order
1696 + *
1697 + * @since 1.0.0
1698 + * @since 3.9.9 Check if user can manage course before updating order.
1699 + *
1700 + * @return void
234 1701 */
235 - public function tutor_update_topic(){
236 - $topic_id = (int) sanitize_text_field($_POST['topic_id']);
237 - $topic_title = sanitize_text_field($_POST['topic_title']);
238 - $topic_summery = wp_kses_post($_POST['topic_summery']);
1702 + public function ajax_update_course_content_order() {
1703 + tutor_utils()->checking_nonce();
239 1704
240 - $topic_attr = array(
241 - 'ID' => $topic_id,
242 - 'post_title' => $topic_title,
243 - 'post_content' => $topic_summery,
244 - );
245 - wp_update_post( $topic_attr );
1705 + $sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
1706 + $sorting_order = json_decode( $sorting_order, true ) ?? array();
246 1707
247 - wp_send_json_success(array('msg' => __('Topic has been updated', 'tutor') ));
1708 + if ( ! tutor_utils()->count( $sorting_order ) ) {
1709 + wp_send_json_error( __( 'Sorting order is required', 'tutor' ) );
1710 + }
1711 +
1712 + $topic_id = (int) isset( $sorting_order[0], $sorting_order[0]['topic_id'] ) ? $sorting_order[0]['topic_id'] : 0;
1713 + $course_id = wp_get_post_parent_id( $topic_id );
1714 +
1715 + if ( ! $topic_id || ! $course_id ) {
1716 + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
1717 + }
1718 +
1719 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ! User::is_admin() ) {
1720 + wp_send_json_error( tutor_utils()->error_message() );
1721 + }
1722 +
1723 + if ( Input::has( 'content_parent' ) ) {
1724 + $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1725 + $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1726 + $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1727 +
1728 + // Update the parent topic id of the content.
1729 + global $wpdb;
1730 + $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1731 + }
1732 +
1733 + // Save course content order.
1734 + $this->save_course_content_order( $sorting_order );
1735 +
1736 + wp_send_json_success();
248 1737 }
249 1738
250 -
251 1739 /**
252 - * @param $columns
1740 + * Restrict new student entry
253 1741 *
1742 + * @since 1.0.0
1743 + * @param mixed $content content.
1744 + *
254 1745 * @return mixed
255 - *
256 - * Add Lesson column
257 1746 */
1747 + public function restrict_new_student_entry( $content ) {
258 1748
259 - public function add_column($columns){
260 - $date_col = $columns['date'];
261 - unset($columns['date']);
262 - $columns['lessons'] = __('Lessons', 'tutor');
263 - $columns['students'] = __('Students', 'tutor');
264 - $columns['price'] = __('Price', 'tutor');
265 - $columns['date'] = $date_col;
1749 + if ( ! tutor_utils()->is_course_fully_booked() ) {
1750 + // No restriction if not fully booked.
1751 + return $content;
1752 + }
266 1753
267 - return $columns;
1754 + 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">' .
1755 + __( 'Fully Booked', 'tutor' )
1756 + . '</div></div>';
268 1757 }
269 1758
270 1759 /**
271 - * @param $column
272 - * @param $post_id
1760 + * Restrict media
273 1761 *
1762 + * @since 1.0.0
1763 + * @param string $where where clause.
1764 + * @return string
274 1765 */
275 - public function custom_lesson_column($column, $post_id ){
276 - if ($column === 'lessons'){
277 - echo tutor_utils()->get_lesson_count_by_course($post_id);
1766 + public function restrict_media( $where ) {
1767 + $action = Input::post( 'action' );
1768 + if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1769 + if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1770 + $where .= ' AND post_author=' . get_current_user_id();
1771 + }
278 1772 }
279 1773
280 - if ($column === 'students'){
281 - echo tutor_utils()->count_enrolled_users_by_course($post_id);
1774 + return $where;
1775 + }
1776 +
1777 + /**
1778 + * Save course content order
1779 + *
1780 + * @since 1.0.0
1781 + * @since 3.9.8 param $order added.
1782 + *
1783 + * @param array $sort_order the lesson and topic order array.
1784 + *
1785 + * @return void
1786 + */
1787 + private function save_course_content_order( $sort_order = array() ) {
1788 + global $wpdb;
1789 +
1790 + if ( ! tutor_utils()->count( $sort_order ) ) {
1791 + return;
282 1792 }
283 1793
284 - if ($column === 'price'){
285 - $price = tutor_utils()->get_course_price($post_id);
1794 + $i = 0;
1795 + foreach ( $sort_order as $topic ) {
1796 + $i++;
1797 + $wpdb->update(
1798 + $wpdb->posts,
1799 + array( 'menu_order' => $i ),
1800 + array( 'ID' => $topic['topic_id'] )
1801 + );
286 1802
287 - if ($price){
288 - if (function_exists('wc_price')){
289 - echo '<span class="tutor-label-success">'.wc_price($price).'</span>';
290 - }else{
291 - echo '<span class="tutor-label-success">'.$price.'</span>';
1803 + /**
1804 + * Removing All lesson with topic
1805 + */
1806 + $wpdb->update(
1807 + $wpdb->posts,
1808 + array( 'post_parent' => 0 ),
1809 + array( 'post_parent' => $topic['topic_id'] )
1810 + );
1811 +
1812 + /**
1813 + * Lesson Attaching with topic ID
1814 + * Sorting lesson
1815 + */
1816 + if ( isset( $topic['lesson_ids'] ) ) {
1817 + $lesson_ids = $topic['lesson_ids'];
1818 + } else {
1819 + $lesson_ids = array();
1820 + }
1821 + if ( count( $lesson_ids ) ) {
1822 + foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1823 + $wpdb->update(
1824 + $wpdb->posts,
1825 + array(
1826 + 'post_parent' => $topic['topic_id'],
1827 + 'menu_order' => $lesson_key,
1828 + ),
1829 + array( 'ID' => $lesson_id )
1830 + );
292 1831 }
293 - }else{
294 - echo 'free';
295 1832 }
296 1833 }
297 1834 }
298 1835
1836 + /**
1837 + * Insert Topic and attached it with Course
1838 + *
1839 + * @since 1.0.0
1840 + *
1841 + * @param integer $post_ID post ID.
1842 + * @param object $post post object.
1843 + *
1844 + * @return void
1845 + */
1846 + public function save_course_meta( $post_ID, $post ) {
1847 + global $wpdb;
299 1848
300 - public function tutor_delete_topic(){
301 - if (!isset($_GET[tutor()->nonce]) || !wp_verify_nonce($_GET[tutor()->nonce], tutor()->nonce_action)) {
302 - exit();
1849 + do_action( 'tutor_save_course', $post_ID, $post );
1850 +
1851 + /**
1852 + * Save course price type
1853 + */
1854 + $price_type = Input::post( 'tutor_course_price_type' );
1855 + if ( $price_type ) {
1856 + update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
303 1857 }
304 - if ( ! isset($_GET['topic_id'])){
305 - exit();
1858 +
1859 + //phpcs:disable WordPress.Security.NonceVerification.Missing
1860 + // Course Duration.
1861 + if ( ! empty( $_POST['course_duration'] ) ) {
1862 + $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1863 + update_post_meta( $post_ID, '_course_duration', $video );
306 1864 }
307 1865
308 - global $wpdb;
1866 + if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1867 + $course_level = Input::post( '_tutor_course_level' );
1868 + update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1869 + }
309 1870
310 - $topic_id = (int) sanitize_text_field($_GET['topic_id']);
311 - $wpdb->update(
312 - $wpdb->posts,
313 - array('post_parent' => 0),
314 - array('post_parent' => $topic_id)
1871 + $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1872 + if ( $additional_data_edit ) {
1873 + if ( ! empty( $_POST['course_benefits'] ) ) {
1874 + $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1875 + update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1876 + } elseif ( ! tutor_is_rest() ) {
1877 + delete_post_meta( $post_ID, '_tutor_course_benefits' );
1878 + }
1879 +
1880 + if ( ! empty( $_POST['course_requirements'] ) ) {
1881 + $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1882 + update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1883 + } elseif ( ! tutor_is_rest() ) {
1884 + delete_post_meta( $post_ID, '_tutor_course_requirements' );
1885 + }
1886 +
1887 + if ( ! empty( $_POST['course_target_audience'] ) ) {
1888 + $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1889 + update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1890 + } elseif ( ! tutor_is_rest() ) {
1891 + delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1892 + }
1893 +
1894 + if ( ! empty( $_POST['course_material_includes'] ) ) {
1895 + $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1896 + update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1897 + } elseif ( ! tutor_is_rest() ) {
1898 + delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1899 + }
1900 + //phpcs:enable WordPress.Security.NonceVerification.Missing
1901 + }
1902 +
1903 + $sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
1904 + $sorting_order = json_decode( $sorting_order, true ) ?? array();
1905 + /**
1906 + * Sorting Topics and lesson
1907 + */
1908 + $this->save_course_content_order( $sorting_order );
1909 +
1910 + // Additional data like course intro video.
1911 + if ( $additional_data_edit ) {
1912 + // Sanitize data through helper method.
1913 + $video = Input::sanitize_array(
1914 + $_POST['video'] ?? array(), //phpcs:ignore
1915 + array(
1916 + 'source_external_url' => 'esc_url',
1917 + 'source_embedded' => 'wp_kses_post',
1918 + ),
1919 + true
1920 + );
1921 + $video_source = tutor_utils()->array_get( 'source', $video );
1922 + if ( -1 !== $video_source ) {
1923 + update_post_meta( $post_ID, '_video', $video );
1924 + } elseif ( ! tutor_is_rest() ) {
1925 + delete_post_meta( $post_ID, '_video' );
1926 + }
1927 + }
1928 +
1929 + /**
1930 + * Adding author to instructor automatically
1931 + */
1932 +
1933 + // Override post author id.
1934 + $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1935 + $attached = (int) $wpdb->get_var(
1936 + $wpdb->prepare(
1937 + "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1938 + WHERE user_id = %d
1939 + AND meta_key = '_tutor_instructor_course_id'
1940 + AND meta_value = %d ",
1941 + $author_id,
1942 + $post_ID
1943 + )
315 1944 );
316 1945
317 - $wpdb->delete(
318 - $wpdb->postmeta,
319 - array('post_id' => $topic_id)
1946 + if ( ! $attached ) {
1947 + add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1948 + }
1949 +
1950 + /**
1951 + * Disable question and answer for this course
1952 + *
1953 + * @since 1.7.0
1954 + */
1955 + if ( $additional_data_edit ) {
1956 + foreach ( $this->additional_meta as $key ) {
1957 + //phpcs:ignore WordPress.Security.NonceVerification.Missing
1958 + update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1959 + }
1960 + }
1961 +
1962 + do_action( 'tutor_save_course_after', $post_ID, $post );
1963 + }
1964 +
1965 + /**
1966 + * Save course topic
1967 + *
1968 + * @since 1.0.0
1969 + * @since 3.0.0 response and input name updated.
1970 + *
1971 + * @return void
1972 + */
1973 + public function tutor_save_topic() {
1974 + tutor_utils()->check_nonce();
1975 +
1976 + $is_update = false;
1977 + $errors = array();
1978 + $topic_title = Input::post( 'title' );
1979 +
1980 + if ( empty( $topic_title ) ) {
1981 + $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
1982 + $this->json_response(
1983 + __( 'Invalid inputs', 'tutor' ),
1984 + $errors,
1985 + HttpHelper::STATUS_UNPROCESSABLE_ENTITY
1986 + );
1987 + }
1988 +
1989 + // Gather parameters.
1990 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1991 + $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1992 + $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
1993 +
1994 + $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
1995 +
1996 + // Validate if user can manage the topic.
1997 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
1998 + wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1999 + }
2000 +
2001 + // Create payload to create/update the topic.
2002 + $post_arr = array(
2003 + 'post_type' => 'topics',
2004 + 'post_title' => $topic_title,
2005 + 'post_content' => $topic_summary,
2006 + 'post_status' => 'publish',
2007 + 'post_author' => get_current_user_id(),
2008 + 'post_parent' => $course_id,
2009 + 'menu_order' => $next_topic_order_id,
320 2010 );
321 2011
322 - wp_delete_post($topic_id);
323 - wp_safe_redirect(wp_get_referer());
2012 + if ( $topic_id ) {
2013 + $is_update = true;
2014 + $post_arr['ID'] = $topic_id;
2015 + }
2016 +
2017 + $current_topic_id = wp_insert_post( $post_arr );
2018 +
2019 + if ( $is_update ) {
2020 + $this->json_response(
2021 + __( 'Topic updated successfully!', 'tutor' ),
2022 + $current_topic_id
2023 + );
2024 + } else {
2025 + $this->json_response(
2026 + __( 'Topic created successfully!', 'tutor' ),
2027 + $current_topic_id,
2028 + HttpHelper::STATUS_CREATED
2029 + );
2030 + }
324 2031 }
325 2032
326 - public function tutor_delete_announcement(){
327 - tutor_utils()->checking_nonce('get');
2033 + /**
2034 + * Delete a course topic
2035 + *
2036 + * @since 1.0.0
2037 + * @since 3.0.0 code refactor and response updated.
2038 + *
2039 + * @return void
2040 + */
2041 + public function tutor_delete_topic() {
2042 + tutor_utils()->check_nonce();
328 2043
329 - $announcement_id = (int) sanitize_text_field($_GET['topic_id']);
2044 + $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
2045 + if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
2046 + $this->json_response(
2047 + tutor_utils()->error_message(),
2048 + null,
2049 + HttpHelper::STATUS_FORBIDDEN
2050 + );
2051 + }
330 2052
331 - wp_delete_post($announcement_id);
332 - wp_safe_redirect(wp_get_referer());
2053 + global $wpdb;
2054 +
2055 + // Assign course ID to orphan content IDs since the topic will be deleted.
2056 + $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
2057 + $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
2058 + foreach ( $content_ids as $content_id ) {
2059 + update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
2060 + // Actually all kind of contents.
2061 + // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
2062 + }
2063 +
2064 + // Set contents under the topic orphan.
2065 + $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
2066 +
2067 + // Then delete the topic from database.
2068 + $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
2069 + wp_delete_post( $topic_id );
2070 +
2071 + $this->json_response(
2072 + __( 'Topic deleted successfully!', 'tutor' )
2073 + );
333 2074 }
334 2075
335 - public function enroll_now(){
336 - //Checking if action comes from Enroll form
337 - if ( ! isset($_POST['tutor_course_action']) || $_POST['tutor_course_action'] !== '_tutor_course_enroll_now' || ! isset($_POST['tutor_course_id']) ){
2076 + /**
2077 + * Handle enroll now action
2078 + *
2079 + * @since 1.0.0
2080 + *
2081 + * @return void
2082 + */
2083 + public function enroll_now() {
2084 +
2085 + if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
338 2086 return;
339 2087 }
340 - //Checking Nonce
2088 +
2089 + // Checking Nonce.
341 2090 tutor_utils()->checking_nonce();
342 2091
343 2092 $user_id = get_current_user_id();
344 - if ( ! $user_id){
345 - exit(__('Please Sign In first', 'tutor'));
2093 + if ( ! $user_id ) {
2094 + exit( esc_html__( 'Please Sign In first', 'tutor' ) );
346 2095 }
347 2096
348 - $course_id = (int) sanitize_text_field($_POST['tutor_course_id']);
349 - $user_id = get_current_user_id();
2097 + $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
350 2098
351 2099 /**
352 2100 * TODO: need to check purchase information
353 2101 */
354 2102
355 - $is_purchasable = tutor_utils()->is_course_purchasable($course_id);
2103 + $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
356 2104
2105 + $course = get_post( $course_id );
2106 +
2107 + if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
2108 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
2109 + }
2110 +
357 2111 /**
358 2112 * If is is not purchasable, it's free, and enroll right now
2113 + * If purchasable, then process purchase.
359 2114 *
360 - * if purchasable, then process purchase.
361 - *
362 2115 * @since: v.1.0.0
363 2116 */
364 - if ($is_purchasable){
365 - //process purchase
2117 + if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2118 + // Process purchase.
366 2119
367 - }else{
368 - //Free enroll
369 - tutor_utils()->do_enroll($course_id);
2120 + } else {
2121 + // Free enroll.
2122 + tutor_utils()->do_enroll( $course_id );
370 2123 }
371 2124
372 2125 $referer_url = wp_get_referer();
373 - wp_redirect($referer_url);
2126 + wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2127 + exit;
374 2128 }
375 2129
376 2130 /**
2131 + * Mark complete completed
377 2132 *
378 - * Mark complete completed
2133 + * @since 1.0.0
379 2134 *
380 - * @since v.1.0.0
2135 + * @since 3.7.1 Filter hook: tutor_user_can_complete_course added
2136 + *
2137 + * @return void
381 2138 */
382 - public function mark_course_complete(){
383 - if ( ! isset($_POST['tutor_action']) || $_POST['tutor_action'] !== 'tutor_complete_course' ){
2139 + public function mark_course_complete() {
2140 + $tutor_action = Input::post( 'tutor_action' );
2141 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2142 + if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
384 2143 return;
385 2144 }
386 - //Checking nonce
2145 +
2146 + $permalink = get_the_permalink( $course_id );
2147 +
2148 + // Checking nonce.
387 2149 tutor_utils()->checking_nonce();
388 2150
389 2151 $user_id = get_current_user_id();
390 2152
391 - //TODO: need to show view if not signed_in
392 - if ( ! $user_id){
393 - die(__('Please Sign-In', 'tutor'));
2153 + // TODO: need to show view if not signed_in.
2154 + if ( ! $user_id ) {
2155 + die( esc_html__( 'Please Sign-In', 'tutor' ) );
394 2156 }
395 2157
396 - $course_id = (int) sanitize_text_field($_POST['course_id']);
2158 + if ( ! tutor_utils()->is_enrolled( $course_id, $user_id ) ) {
2159 + die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2160 + }
397 2161
398 - do_action('tutor_course_complete_before', $course_id);
399 2162 /**
400 - * Marking course completed at Comment
2163 + * Filter hook provided to restrict course completion. This is useful
2164 + * for specific cases like prerequisites. WP_Error should be returned
2165 + * from the filter value to prevent the completion.
401 2166 */
2167 + $can_complete = apply_filters( 'tutor_user_can_complete_course', true, $user_id, $course_id );
402 2168
403 - global $wpdb;
2169 + if ( is_wp_error( $can_complete ) ) {
2170 + tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2171 + } else {
2172 + CourseModel::mark_course_as_completed( $course_id, $user_id );
2173 + // Set temporary identifier to show review pop up.
2174 + self::set_review_popup_data( $user_id, $course_id, $permalink );
404 2175
405 - $date = date("Y-m-d H:i:s");
2176 + wp_safe_redirect( $permalink );
2177 + exit;
2178 + }
2179 + }
406 2180
407 - //Making sure that, hash is unique
408 - do{
409 - $hash = substr(md5(wp_generate_password(32).$date.$course_id.$user_id), 0, 16);
410 - $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}' ");
411 - }while($hasHash > 0);
2181 + /**
2182 + * Set data for review popup.
2183 + *
2184 + * @since 2.2.5
2185 + * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2186 + *
2187 + * @param int $user_id user id.
2188 + * @param int $course_id course id.
2189 + *
2190 + * @return void
2191 + */
2192 + public static function set_review_popup_data( $user_id, $course_id ) {
2193 + if ( get_tutor_option( 'enable_course_review' ) ) {
2194 + $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2195 + if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2196 + $meta_key = User::get_review_popup_meta( $course_id );
2197 + add_user_meta( $user_id, $meta_key, $course_id, true );
2198 + }
2199 + }
2200 + }
412 2201
413 - $data = array(
414 - 'comment_post_ID' => $course_id,
415 - 'comment_author' => $user_id,
416 - 'comment_date' => $date,
417 - 'comment_date_gmt' => get_gmt_from_date($date),
418 - 'comment_content' => $hash, //Identification Hash
419 - 'comment_approved' => 'approved',
420 - 'comment_agent' => 'TutorLMSPlugin',
421 - 'comment_type' => 'course_completed',
422 - 'user_id' => $user_id,
2202 + /**
2203 + * Popup review form on course details
2204 + *
2205 + * @since 1.0.0
2206 + * @return void
2207 + */
2208 + public function popup_review_form() {
2209 + if ( is_user_logged_in() ) {
2210 + $user_id = get_current_user_id();
2211 + $course_id = get_the_ID();
2212 + $meta_key = User::get_review_popup_meta( $course_id );
2213 + $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2214 +
2215 + if ( is_single() && $course_id === $review_course_id ) {
2216 + include tutor()->path . 'views/modal/review.php';
2217 + }
2218 + }
2219 + }
2220 +
2221 + /**
2222 + * Review popup data clear
2223 + *
2224 + * @since 2.4.0
2225 + *
2226 + * @return void
2227 + */
2228 + public function clear_review_popup_data() {
2229 + tutils()->checking_nonce();
2230 +
2231 + if ( is_user_logged_in() ) {
2232 + $user_id = get_current_user_id();
2233 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2234 +
2235 + if ( $course_id ) {
2236 + $meta_key = User::get_review_popup_meta( $course_id );
2237 + delete_user_meta( $user_id, $meta_key, $course_id );
2238 + }
2239 +
2240 + wp_send_json_success();
2241 + }
2242 + }
2243 +
2244 + /**
2245 + * Delete course delete from frontend dashboard
2246 + *
2247 + * @since 2.0.0
2248 + * @return void
2249 + */
2250 + public function tutor_delete_dashboard_course() {
2251 + tutor_utils()->checking_nonce();
2252 +
2253 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2254 + if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2255 + wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2256 + }
2257 +
2258 + /**
2259 + * Co-instructor can not delete a course
2260 + *
2261 + * @since 2.1.6
2262 + */
2263 + if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2264 + wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2265 + }
2266 +
2267 + // Check if user is only an instructor.
2268 + if ( ! current_user_can( 'administrator' ) ) {
2269 + // Check if instructor can trash course.
2270 + $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2271 +
2272 + if ( ! $can_trash_post ) {
2273 + wp_send_json_error( tutor_utils()->error_message() );
2274 + }
2275 + }
2276 +
2277 + $trash_course = wp_update_post(
2278 + array(
2279 + 'ID' => $course_id,
2280 + 'post_status' => 'trash',
2281 + )
423 2282 );
424 2283
425 - $wpdb->insert($wpdb->comments, $data);
2284 + if ( $trash_course ) {
2285 + wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2286 + }
2287 + wp_send_json_success();
2288 + }
426 2289
427 - do_action('tutor_course_complete_after', $course_id);
2290 + /**
2291 + * Main author change from gutenberg editor
2292 + *
2293 + * @since 2.0.0
2294 + *
2295 + * @param array $data data.
2296 + * @param array $postarr post array.
2297 + *
2298 + * @return mixed
2299 + */
2300 + public function tutor_add_gutenberg_author( $data, $postarr ) {
2301 + $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2302 + $post_type = $postarr['post_type'];
2303 + $courses_post_type = tutor()->course_post_type;
428 2304
429 - wp_redirect(get_the_permalink($course_id));
2305 + if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2306 + return $data;
2307 + }
2308 +
2309 + /**
2310 + * Only admin can change main author
2311 + */
2312 + if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2313 + global $wpdb;
2314 + $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2315 + $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2316 +
2317 + if ( $post_author > 0 ) {
2318 + $data['post_author'] = $post_author;
2319 + } else {
2320 + $data['post_author'] = get_current_user_id();
2321 + }
2322 + }
2323 +
2324 + return $data;
430 2325 }
431 2326
432 -
433 - public function tutor_load_instructors_modal(){
2327 +
2328 + /**
2329 + * Attach product with course when course save from frontend or backend.
2330 + *
2331 + * @since 1.3.4
2332 + *
2333 + * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2334 + *
2335 + * @param integer $post_ID course ID.
2336 + * @param array $post_data created course post details.
2337 + *
2338 + * @return void
2339 + */
2340 + public function attach_product_with_course( $post_ID, $post_data ) {
2341 + $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2342 + $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2343 +
2344 + /**
2345 + * For native monetization, just return
2346 + * No need to attach anything.
2347 + */
2348 + if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2349 + return;
2350 + }
2351 +
2352 + /**
2353 + * When course moved paid to free
2354 + * Keep the product linked and return.
2355 + */
2356 + if ( -1 === $product_id ) {
2357 + return;
2358 + }
2359 +
2360 + /**
2361 + * Free user can only select product from dropdown
2362 + */
2363 + if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2364 + if ( $product_id > 0 ) {
2365 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2366 + }
2367 +
2368 + return;
2369 + }
2370 +
2371 + $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2372 + $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2373 + $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2374 +
2375 + if ( ! $course_price || $sale_price >= $course_price ) {
2376 + return;
2377 + }
2378 +
2379 + $course = get_post( $post_ID );
2380 +
2381 + update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2382 +
2383 + if ( 'wc' === $monetize_by ) {
2384 +
2385 + $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2386 +
2387 + if ( $is_update ) {
2388 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2389 +
2390 + $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2391 + $product_obj = wc_get_product( $product_id );
2392 + if ( $product_obj->is_type( 'subscription' ) ) {
2393 + update_post_meta( $product_id, '_subscription_price', $course_price );
2394 + }
2395 +
2396 + // Set course regular & sale price.
2397 + self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2398 + } else {
2399 + // Create new WC product name with course title.
2400 + $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2401 + if ( $product_id ) {
2402 + $product_obj = wc_get_product( $product_id );
2403 +
2404 + self::sync_course_with_wc_product( $post_ID, $product_id );
2405 +
2406 + // Set course regular & sale price.
2407 + self::set_course_regular_and_sale_price( $post_ID, $product_obj->get_regular_price(), $product_obj->get_sale_price() );
2408 + }
2409 + }
2410 +
2411 + $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2412 + if ( $product_id && $course_post_thumbnail ) {
2413 + set_post_thumbnail( $product_id, $course_post_thumbnail );
2414 + }
2415 + } elseif ( 'edd' === $monetize_by ) {
2416 +
2417 + $is_update = false;
2418 +
2419 + if ( $attached_product_id ) {
2420 + $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2421 + if ( $edd_price ) {
2422 + $is_update = true;
2423 + }
2424 + }
2425 +
2426 + if ( $is_update ) {
2427 + // Update the product.
2428 + update_post_meta( $attached_product_id, 'edd_price', $course_price );
2429 + } else {
2430 + // Create new product.
2431 +
2432 + $post_arr = array(
2433 + 'post_type' => 'download',
2434 + 'post_title' => $course->post_title,
2435 + 'post_status' => 'publish',
2436 + 'post_author' => get_current_user_id(),
2437 + );
2438 + $download_id = wp_insert_post( $post_arr );
2439 + if ( $download_id ) {
2440 + // EDD edd_price.
2441 + update_post_meta( $download_id, 'edd_price', $course_price );
2442 +
2443 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2444 + // Mark product for EDD.
2445 + update_post_meta( $download_id, '_tutor_product', 'yes' );
2446 +
2447 + $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2448 + if ( $course_post_thumbnail ) {
2449 + set_post_thumbnail( $download_id, $course_post_thumbnail );
2450 + }
2451 + }
2452 + }
2453 + }
2454 + }
2455 +
2456 + /**
2457 + * Add Course level to course settings
2458 + *
2459 + * @since 1.4.1
2460 + *
2461 + * @param array $args arguments.
2462 + * @return array
2463 + */
2464 + public function add_course_level_to_settings( $args ) {
2465 + $course_id = get_the_ID();
2466 + $levels = tutor_utils()->course_levels();
2467 + $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2468 +
2469 + $args['general']['fields']['_tutor_course_level'] = array(
2470 + 'type' => 'select',
2471 + 'label' => __( 'Difficulty Level', 'tutor' ),
2472 + 'label_title' => __( 'Enable', 'tutor' ),
2473 + 'options' => $levels,
2474 + 'value' => $course_level ? $course_level : 'intermediate',
2475 + 'desc' => __( 'Course difficulty level', 'tutor' ),
2476 + );
2477 +
2478 + return $args;
2479 + }
2480 +
2481 + /**
2482 + * Check if course starting
2483 + *
2484 + * @since 1.4.8
2485 + * @return void
2486 + */
2487 + public function tutor_lesson_load_before() {
2488 + $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2489 + $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2490 + if ( is_user_logged_in() ) {
2491 + $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2492 + if ( ! $completed_lessons && ! $is_course_started ) {
2493 + update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2494 + do_action( 'tutor/course/started', $course_id );
2495 + }
2496 + }
2497 + }
2498 +
2499 + /**
2500 + * Add Course level to course settings
2501 + *
2502 + * @since 1.4.8
2503 + * @return void
2504 + */
2505 + public function course_elements_enable_disable() {
2506 + add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2507 + add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2508 + add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2509 + add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2510 + add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2511 + add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2512 + add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2513 + }
2514 +
2515 + /**
2516 + * Enable disable course progress bar
2517 + *
2518 + * @since 1.4.8
2519 + *
2520 + * @param string $html HTML string.
2521 + * @return string
2522 + */
2523 + public function enable_disable_course_progress_bar( $html ) {
2524 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2525 + if ( $disable_option ) {
2526 + return '';
2527 + }
2528 + return $html;
2529 + }
2530 +
2531 + /**
2532 + * Enable disable material includes
2533 + *
2534 + * @since 1.4.8
2535 + *
2536 + * @param string $html HTML string.
2537 + * @return string
2538 + */
2539 + public function enable_disable_material_includes( $html ) {
2540 + $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2541 + if ( $disable_option ) {
2542 + return '';
2543 + }
2544 + return $html;
2545 + }
2546 +
2547 + /**
2548 + * Enable disable course content
2549 + *
2550 + * @since 1.4.8
2551 + *
2552 + * @param string $html HTML string.
2553 + * @return string
2554 + */
2555 + public function enable_disable_course_content( $html ) {
2556 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2557 + if ( $disable_option ) {
2558 + return '';
2559 + }
2560 + return $html;
2561 + }
2562 +
2563 + /**
2564 + * Enable disable course benefits
2565 + *
2566 + * @since 1.4.8
2567 + *
2568 + * @param string $html HTML string.
2569 + * @return string
2570 + */
2571 + public function enable_disable_course_benefits( $html ) {
2572 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2573 + if ( $disable_option ) {
2574 + return '';
2575 + }
2576 + return $html;
2577 + }
2578 +
2579 + /**
2580 + * Enable disable course requirements
2581 + *
2582 + * @since 1.4.8
2583 + *
2584 + * @param string $html HTML string.
2585 + * @return string
2586 + */
2587 + public function enable_disable_course_requirements( $html ) {
2588 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2589 + if ( $disable_option ) {
2590 + return '';
2591 + }
2592 + return $html;
2593 + }
2594 +
2595 + /**
2596 + * Enable disable course target audience
2597 + *
2598 + * @since 1.4.8
2599 + *
2600 + * @param string $html HTML string.
2601 + * @return string
2602 + */
2603 + public function enable_disable_course_target_audience( $html ) {
2604 + $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2605 + if ( $disable_option ) {
2606 + return '';
2607 + }
2608 + return $html;
2609 + }
2610 +
2611 + /**
2612 + * Enable disable course nav items
2613 + *
2614 + * @since 1.4.8
2615 + *
2616 + * @param array $items item list.
2617 + * @param integer $course_id course ID.
2618 + *
2619 + * @return array
2620 + */
2621 + public function enable_disable_course_nav_items( $items, $course_id ) {
2622 + global $wp_query, $post;
2623 + $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2624 + $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2625 + $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2626 +
2627 + // Whether Q&A enabled.
2628 + if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2629 + if ( tutor_utils()->array_get( 'questions', $items ) ) {
2630 + unset( $items['questions'] );
2631 + }
2632 + }
2633 +
2634 + // Whether announcment enabled.
2635 + if ( $disable_course_announcements ) {
2636 + if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2637 + unset( $items['announcements'] );
2638 + }
2639 + }
2640 +
2641 + // Hide review section if disabled.
2642 + if ( ! get_tutor_option( 'enable_course_review' ) ) {
2643 + unset( $items['reviews'] );
2644 + }
2645 +
2646 + // Whether enrollment require.
2647 + $is_enrolled = tutor_utils()->is_enrolled();
2648 +
2649 + return array_filter(
2650 + $items,
2651 + function ( $item ) use ( $is_enrolled ) {
2652 + if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2653 + return $is_enrolled;
2654 + }
2655 + return true;
2656 + }
2657 + );
2658 + }
2659 +
2660 + /**
2661 + * Filter product in shop page
2662 + *
2663 + * @since 1.4.9
2664 + * @return void|null
2665 + */
2666 + public function filter_product_in_shop_page() {
2667 + $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2668 + if ( ! $hide_course_from_shop_page ) {
2669 + return;
2670 + }
2671 + add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2672 + add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2673 + add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2674 + }
2675 +
2676 +
2677 + /**
2678 + * Tutor product meta query
2679 + *
2680 + * @since 1.4.9
2681 + * @return array
2682 + */
2683 + public function tutor_product_meta_query() {
2684 + $meta_query = array(
2685 + 'key' => '_tutor_product',
2686 + 'compare' => 'NOT EXISTS',
2687 + );
2688 + return $meta_query;
2689 + }
2690 +
2691 + /**
2692 + * Filter product in woocommerce shop page
2693 + *
2694 + * @since 1.4.9
2695 + *
2696 + * @param \WP_Query $wp_query WP Query instance.
2697 + * @return \WP_Query
2698 + */
2699 + public function filter_woocommerce_product_query( $wp_query ) {
2700 + $product_ids = $this->get_connected_wc_product_ids();
2701 + $wp_query->set( 'post__not_in', $product_ids );
2702 + return $wp_query;
2703 + }
2704 +
2705 + /**
2706 + * Get connected woocommerce product ids for course and course bundle
2707 + *
2708 + * @since 2.7.2
2709 + *
2710 + * @return array
2711 + */
2712 + public function get_connected_wc_product_ids() {
434 2713 global $wpdb;
435 2714
436 - $course_id = (int) sanitize_text_field($_POST['course_id']);
437 - $search_terms = sanitize_text_field(tutor_utils()->avalue_dot('search_terms', $_POST));
2715 + $results = $wpdb->get_results(
2716 + $wpdb->prepare(
2717 + "SELECT DISTINCT pm.meta_value product_id
2718 + FROM {$wpdb->posts} p
2719 + INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2720 + AND pm.meta_key = %s
2721 + WHERE post_type IN( 'courses','course-bundle' )",
2722 + '_tutor_course_product_id'
2723 + )
2724 + );
438 2725
439 - $saved_instructors = tutor_utils()->get_instructors_by_course($course_id);
2726 + $ids = array();
2727 + if ( is_array( $results ) && count( $results ) ) {
2728 + $ids = array_column( $results, 'product_id' );
2729 + }
440 2730
441 - $instructors = array();
2731 + return $ids;
2732 + }
442 2733
2734 + /**
2735 + * Filter product in edd downloads shortcode page
2736 + *
2737 + * @since 1.4.9
2738 + *
2739 + * @param \WP_Query $query WP Query instance.
2740 + * @return \WP_Query
2741 + */
2742 + public function filter_edd_downloads_query( $query ) {
2743 + $query['meta_query'][] = $this->tutor_product_meta_query();
2744 + return $query;
2745 + }
443 2746
444 - $not_in_sql = apply_filters('tutor_instructor_query_when_exists', " AND ID <1 ");
2747 + /**
2748 + * Filter product in edd downloads archive page
2749 + *
2750 + * @since 1.4.9
2751 + *
2752 + * @param \WP_Query $wp_query WP Query instance.
2753 + * @return \WP_Query
2754 + */
2755 + public function filter_archive_meta_query( $wp_query ) {
2756 + if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2757 + $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2758 + }
2759 + return $wp_query;
2760 + }
445 2761
446 - if ($saved_instructors){
447 - $saved_instructors_ids = wp_list_pluck($saved_instructors, 'ID');
448 - $instructor_not_in_ids = implode(',', $saved_instructors_ids);
449 - $not_in_sql .= "AND ID NOT IN($instructor_not_in_ids) ";
2762 + /**
2763 + * Removed course price if already enrolled at single course
2764 + *
2765 + * @since 1.5.8
2766 + *
2767 + * @param string $html HTML string.
2768 + * @return string
2769 + */
2770 + public function remove_price_if_enrolled( $html ) {
2771 + $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2772 +
2773 + if ( $should_removed ) {
2774 + $course_id = get_the_ID();
2775 + $enrolled = tutor_utils()->is_enrolled( $course_id );
2776 + if ( $enrolled ) {
2777 + $html = '';
2778 + }
450 2779 }
2780 + return $html;
2781 + }
451 2782
452 - $search_sql = '';
453 - if ($search_terms){
454 - $search_sql = "AND user_login like '%{$search_terms}%' or user_nicename like '%{$search_terms}%' or display_name like '%{$search_terms}%' ";
2783 + /**
2784 + * Check if all lessons and quizzes done before mark course complete.
2785 + *
2786 + * @since 1.5.8
2787 + *
2788 + * @param string $html HTML string.
2789 + * @return string
2790 + */
2791 + public function tutor_lms_hide_course_complete_btn( $html ) {
2792 +
2793 + $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2794 + if ( 'strict' !== $completion_mode ) {
2795 + return $html;
455 2796 }
456 2797
457 - $instructors = $wpdb->get_results("select ID, display_name from {$wpdb->users}
458 - INNER JOIN {$wpdb->usermeta} ON ID = user_id AND meta_key = '_tutor_instructor_status' AND meta_value = 'approved'
459 - WHERE ID > 0 {$not_in_sql} {$search_sql} limit 10 ");
2798 + $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2799 + $lesson_count = tutor_utils()->get_lesson_count_by_course();
460 2800
461 - $output = '';
462 - if (is_array($instructors) && count($instructors)){
463 - $instructor_output = '';
464 - foreach ($instructors as $instructor){
465 - $instructor_output .= "<p><label><input type='radio' name='tutor_instructor_ids[]' value='{$instructor->ID}' > {$instructor->display_name} </label></p>";
2801 + if ( $completed_lesson < $lesson_count ) {
2802 + return '<div class="tutor-alert tutor-warning tutor-mt-28">
2803 + <div class="tutor-alert-text">
2804 + <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2805 + <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2806 + </div>
2807 + </div>';
2808 + }
2809 +
2810 + $quizzes = array();
2811 + $assignments = array();
2812 +
2813 + $course_contents = tutor_utils()->get_course_contents_by_id();
2814 + if ( tutor_utils()->count( $course_contents ) ) {
2815 + foreach ( $course_contents as $content ) {
2816 + if ( 'tutor_quiz' === $content->post_type ) {
2817 + $quizzes[] = $content;
2818 + }
2819 + if ( 'tutor_assignments' === $content->post_type ) {
2820 + $assignments[] = $content;
2821 + }
466 2822 }
2823 + }
467 2824
468 - $output .= apply_filters('tutor_course_instructors_html', $instructor_output, $instructors);
469 - $output .= '<p class="quiz-search-suggest-text">'.__('Search to get the specific instructors', 'tutor').'</p>';
2825 + $required_assignment_pass = 0;
470 2826
471 - }else{
472 - $output .= __('No instructor available or you have already added maximum instructors', 'tutor');
2827 + foreach ( $assignments as $row ) {
2828 +
2829 + $assignment_submission = tutor_utils()->is_assignment_submitted( $row->ID );
2830 + $is_reviewed_by_instructor = ! count( $assignment_submission )
2831 + ? false
2832 + : get_comment_meta( $assignment_submission[0]->comment_ID, 'evaluate_time', true );
2833 +
2834 + if ( $assignment_submission && $is_reviewed_by_instructor ) {
2835 + $pass_mark = tutor_utils()->get_assignment_option( $row->ID, 'pass_mark' );
2836 + $has_passed = false;
2837 + foreach ( $assignment_submission as $submission ) {
2838 + $given_mark = (int) get_comment_meta( $submission->comment_ID, 'assignment_mark', true );
2839 + if ( $given_mark >= $pass_mark ) {
2840 + $has_passed = true;
2841 + break;
2842 + }
2843 + }
2844 + if ( ! $has_passed ) {
2845 + $required_assignment_pass++;
2846 + }
2847 + } else {
2848 + $required_assignment_pass++;
2849 + }
473 2850 }
474 2851
2852 + $is_quiz_pass = true;
2853 + $required_quiz_pass = 0;
475 2854
476 - if ( ! defined('TUTOR_MT_VERSION')){
477 - $output .= '<p class="tutor-notice-warning" style="margin-top: 50px; font-size: 14px;">'. sprintf( __('To add unlimited multiple instructors in your course, get %sTutor LMS Pro%s', 'tutor'), '<a href="https://www.themeum.com/product/tutor-lms" target="_blank">', "</a>" ) .'</p>';
2855 + if ( tutor_utils()->count( $quizzes ) ) {
2856 + foreach ( $quizzes as $quiz ) {
2857 +
2858 + $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2859 + if ( $attempt ) {
2860 + $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2861 + $earned_percentage = QuizModel::calculate_attempt_earned_percentage( $attempt );
2862 +
2863 + if ( $earned_percentage < $passing_grade ) {
2864 + $required_quiz_pass++;
2865 + $is_quiz_pass = false;
2866 + }
2867 + } else {
2868 + $required_quiz_pass++;
2869 + $is_quiz_pass = false;
2870 + }
2871 + }
478 2872 }
479 2873
480 - wp_send_json_success(array('output' => $output));
2874 + if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2875 + $_msg = '';
2876 + $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2877 + $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2878 +
2879 + if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2880 + /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2881 + $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2882 + }
2883 +
2884 + if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2885 + //phpcs:ignore
2886 + $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2887 + }
2888 +
2889 + if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2890 + /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2891 + $_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 );
2892 + }
2893 +
2894 + return '<div class="tutor-alert tutor-warning tutor-mt-28">
2895 + <div class="tutor-alert-text">
2896 + <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2897 + <span>' . $_msg . '</span>
2898 + </div>
2899 + </div>';
2900 + }
2901 +
2902 + return $html;
481 2903 }
482 2904
483 - public function tutor_add_instructors_to_course(){
484 - $course_id = (int) sanitize_text_field($_POST['course_id']);
485 - $instructor_ids = tutor_utils()->avalue_dot('tutor_instructor_ids', $_POST);
2905 + /**
2906 + * Generate Gradebook
2907 + *
2908 + * @since 1.5.8
2909 + *
2910 + * @param string $html HTML string.
2911 + * @return string
2912 + */
2913 + public function get_generate_greadbook( $html ) {
2914 + if ( ! tutor_utils()->is_completed_course() ) {
2915 + return '';
2916 + }
2917 + return $html;
2918 + }
486 2919
487 - if (is_array($instructor_ids) && count($instructor_ids)){
488 - foreach ($instructor_ids as $instructor_id){
489 - add_user_meta($instructor_id, '_tutor_instructor_course_id', $course_id);
2920 + /**
2921 + * Add social share content in header
2922 + *
2923 + * @since 1.6.3
2924 + * @return void
2925 + */
2926 + public function social_share_content() {
2927 + global $wp_query, $post;
2928 + if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2929 + <!--Facebook-->
2930 + <meta property="og:type" content="website"/>
2931 + <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2932 + <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2933 + <!--Twitter-->
2934 + <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2935 + <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2936 + <!--Google+-->
2937 + <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2938 + <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2939 + <?php
2940 + }
2941 + }
2942 +
2943 + /**
2944 + * Delete associated enrollment
2945 + *
2946 + * @since 1.8.2
2947 + *
2948 + * @param integer $post_id post ID.
2949 + * @return void
2950 + */
2951 + public function delete_associated_enrollment( $post_id ) {
2952 + global $wpdb;
2953 +
2954 + $enroll_id = $wpdb->get_var(
2955 + $wpdb->prepare(
2956 + "SELECT
2957 + post_id
2958 + FROM
2959 + {$wpdb->postmeta}
2960 + WHERE
2961 + meta_key='_tutor_enrolled_by_order_id'
2962 + AND meta_value = %d
2963 + ",
2964 + $post_id
2965 + )
2966 + );
2967 +
2968 + if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2969 +
2970 + $course_id = get_post_field( 'post_parent', $enroll_id );
2971 + $user_id = get_post_field( 'post_author', $enroll_id );
2972 +
2973 + tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2974 + }
2975 + }
2976 +
2977 + /**
2978 + * Reset course progress.
2979 + *
2980 + * @since 1.5.8
2981 + * @return void
2982 + */
2983 + public function tutor_reset_course_progress() {
2984 + tutor_utils()->checking_nonce();
2985 + $course_id = Input::post( 'course_id' );
2986 +
2987 + if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2988 + wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2989 + return;
2990 + }
2991 +
2992 + tutor_utils()->delete_course_progress( $course_id );
2993 + wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2994 + }
2995 +
2996 + /**
2997 + * Do enroll if guest attempt to enroll and course is free
2998 + *
2999 + * @since 1.9.8
3000 + *
3001 + * @param integer $course_id course ID.
3002 + * @param integer $user_id user ID.
3003 +
3004 + * @return void
3005 + */
3006 + public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
3007 + $course_id = sanitize_text_field( $course_id );
3008 + $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
3009 +
3010 + if ( $course_id && $is_allowed ) {
3011 + $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
3012 + if ( ! $is_purchasable ) {
3013 + tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
3014 + do_action( 'guest_attempt_after_enrollment', $course_id );
490 3015 }
491 3016 }
3017 + }
492 3018
493 - $saved_instructors = tutor_utils()->get_instructors_by_course($course_id);
494 - $output = '';
3019 + /**
3020 + * Handle course enrollment
3021 + *
3022 + * @since 2.1.0
3023 + * @return void
3024 + */
3025 + public function course_enrollment() {
3026 + tutor_utils()->checking_nonce();
495 3027
496 - if ($saved_instructors){
497 - foreach ($saved_instructors as $t){
3028 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3029 + $user_id = get_current_user_id();
498 3030
499 - $output .= '<div id="added-instructor-id-'.$t->ID.'" class="added-instructor-item added-instructor-item-'.$t->ID.'" data-instructor-id="'.$t->ID.'">
500 - <span class="instructor-icon">'.get_avatar($t->ID, 30).'</span>
501 - <span class="instructor-name"> '.$t->display_name.' </span>
502 - <span class="instructor-control">
503 - <a href="javascript:;" class="tutor-instructor-delete-btn"><i class="tutor-icon-garbage"></i></a>
504 - </span>
505 - </div>';
3031 + if ( $course_id ) {
3032 + $password_protected = post_password_required( $course_id );
3033 + if ( $password_protected ) {
3034 + wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
506 3035 }
3036 +
3037 + $course = get_post( $course_id );
3038 +
3039 + if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3040 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3041 + }
3042 +
3043 + /**
3044 + * This check was added to address a security issue where users could
3045 + * enroll in a course via an AJAX call without purchasing it.
3046 + *
3047 + * To prevent this, we now verify whether the course is paid.
3048 + * Additionally, we check if the user is already enrolled, since
3049 + * Tutor's default behavior enrolls users automatically upon purchase.
3050 + *
3051 + * @since 3.9.4
3052 + */
3053 + if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3054 + $is_enrolled = (bool) tutor_utils()->is_enrolled( $course_id, $user_id );
3055 +
3056 + if ( ! $is_enrolled ) {
3057 + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3058 + }
3059 + }
3060 +
3061 + $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
3062 + if ( $enroll ) {
3063 + wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3064 + } else {
3065 + wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3066 + }
3067 + } else {
3068 + wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
507 3069 }
3070 + }
508 3071
509 - wp_send_json_success(array('output' => $output));
3072 + /**
3073 + * After trash a course direct to the course list page
3074 + *
3075 + * @since 2.1.7
3076 + *
3077 + * @param integer $post_id int course id.
3078 + *
3079 + * @return void
3080 + */
3081 + public static function redirect_to_course_list_page( int $post_id ): void {
3082 + $post = get_post( $post_id );
3083 + if ( tutor()->course_post_type === $post->post_type ) {
3084 + $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
3085 + if ( ! $is_gutenberg_enabled ) {
3086 + wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
3087 + exit;
3088 + }
3089 + }
510 3090 }
511 3091
512 - public function detach_instructor_from_course(){
513 - global $wpdb;
3092 + /**
3093 + * Create or update WooCommerce product
3094 + *
3095 + * If product id not set it will create new one.
3096 + *
3097 + * @since 2.2.0
3098 + *
3099 + * @param string $title product title.
3100 + * @param string $reg_price product price.
3101 + * @param string $sale_price product sale price.
3102 + * @param int $product_id product ID.
3103 + * @param string $status product status.
3104 + *
3105 + * @return integer Product id or return 0 if WC not exists
3106 + */
3107 + public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
3108 + if ( ! tutor_utils()->has_wc() ) {
3109 + return 0;
3110 + }
514 3111
515 - $instructor_id = (int) sanitize_text_field($_POST['instructor_id']);
516 - $course_id = (int) sanitize_text_field($_POST['course_id']);
3112 + $product_obj = new \WC_Product();
3113 + if ( $product_id ) {
3114 + $product_obj = wc_get_product( $product_id );
3115 + }
517 3116
518 - $wpdb->delete($wpdb->usermeta, array('user_id' => $instructor_id, 'meta_key' => '_tutor_instructor_course_id', 'meta_value' => $course_id) );
519 - wp_send_json_success();
3117 + $product_obj->set_name( $title );
3118 + $product_obj->set_status( $status );
3119 + $product_obj->set_price( $reg_price );
3120 + $product_obj->set_regular_price( $reg_price );
3121 +
3122 + if ( $sale_price > 0 ) {
3123 + $product_obj->set_sale_price( $sale_price );
3124 + } else {
3125 + $product_obj->set_sale_price( null );
3126 + }
3127 +
3128 + $product_obj->set_sold_individually( true );
3129 +
3130 + return $product_obj->save();
520 3131 }
521 3132
3133 + /**
3134 + * Get course/bundle mini info
3135 + *
3136 + * @since 3.0.0
3137 + *
3138 + * @param object $post Course or bundle post.
3139 + *
3140 + * @return array
3141 + */
3142 + public static function get_mini_info( object $post ) {
3143 + $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3144 + $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3145 + $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3146 + $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
522 3147
523 -}
3148 + $info = array(
3149 + 'id' => $post->ID,
3150 + 'title' => $post->post_title,
3151 + 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3152 + 'is_purchasable' => $is_purchasable,
3153 + 'regular_price' => $regular_price,
3154 + 'sale_price' => $sale_price,
3155 + );
3156 +
3157 + $card_data = apply_filters( 'tutor_course_mini_info', $info, $post );
3158 +
3159 + return $card_data;
3160 + }
3161 +
3162 + /**
3163 + * Get course/bundle card data
3164 + *
3165 + * This method will return all data that contain in
3166 + * course card
3167 + *
3168 + * @since 3.0.0
3169 + *
3170 + * @param object $post Course or bundle post.
3171 + *
3172 + * @return array
3173 + */
3174 + public static function get_card_data( object $post ) {
3175 + $info = self::get_mini_info( $post );
3176 +
3177 + $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3178 + $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3179 + $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3180 +
3181 + $card_data = apply_filters( 'tutor_course_card_data', $info, $post );
3182 +
3183 + return $card_data;
3184 + }
3185 +
3186 + /**
3187 + * Filter user list access for instructor
3188 + *
3189 + * @since 3.0.0
3190 + *
3191 + * @param bool $access access.
3192 + *
3193 + * @return bool
3194 + */
3195 + public function user_list_access_for_instructor( $access ) {
3196 + $is_instructor = User::is_instructor();
3197 + return $access || $is_instructor;
3198 + }
3199 +
3200 + /**
3201 + * Filter user list args for instructor
3202 + *
3203 + * @since 3.0.0
3204 + *
3205 + * @param array $args args.
3206 + *
3207 + * @return array
3208 + */
3209 + public function user_list_args_for_instructor( $args ) {
3210 + if ( User::is_instructor() ) {
3211 + if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3212 + unset( $args['fields']['user_email'] );
3213 + }
3214 + }
3215 +
3216 + $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3217 + if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3218 + $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3219 + }
3220 +
3221 + return $args;
3222 + }
3223 +
3224 + /**
3225 + * Get a list of possible course status.
3226 + *
3227 + * @since 3.6.2
3228 + *
3229 + * @return array
3230 + */
3231 + public static function course_status_list() {
3232 + return array(
3233 + 'publish',
3234 + 'private',
3235 + 'draft',
3236 + 'trash',
3237 + 'pending',
3238 + 'future',
3239 + );
3240 + }
3241 +
3242 + /**
3243 + * Link a course/bundle post to a WooCommerce product.
3244 + *
3245 + * @since 3.8.2
3246 + *
3247 + * @param int $post_ID The WordPress post ID of the course.
3248 + * @param int $product_id The WooCommerce product ID to associate with the course.
3249 + * @return void
3250 + */
3251 + public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3252 +
3253 + update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
3254 +
3255 + // Mark product for woocommerce.
3256 + update_post_meta( $product_id, '_virtual', 'yes' );
3257 + update_post_meta( $product_id, '_tutor_product', 'yes' );
3258 + }
3259 +
3260 + /**
3261 + * Map Tutor's course prices to WooCommerce.
3262 + *
3263 + * @since 3.8.2
3264 + *
3265 + * @param int $post_ID The WordPress post ID of the course.
3266 + * @param string|int|float $regular_price The regular price.
3267 + * @param string|int|float $sale_price The sale price.
3268 + * @return void
3269 + */
3270 + private static function set_course_regular_and_sale_price( $post_ID, $regular_price, $sale_price ) {
3271 +
3272 + // Set course regular & sale price.
3273 + update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3274 + update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3275 + }
3276 +}