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