PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.6.1
Tutor LMS – eLearning and online course solution v3.6.1
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
3108 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 wp_set_script_translations( 'tutor-course-builder', 'tutor', tutor()->path . 'languages/' );
1404
1405 wp_localize_script(
1406 'mce-view',
1407 'mceViewL10n',
1408 array(
1409 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(),
1410 )
1411 );
1412 }
1413
1414 /**
1415 * Localize custom course builder data for _tutorobject.
1416 *
1417 * @since 3.3.1
1418 *
1419 * @param array $data the localized data.
1420 *
1421 * @return array
1422 */
1423 public function localize_course_builder_data( $data ) {
1424 global $pagenow;
1425
1426 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1427 $backend_builder = is_admin() && 'admin.php' === $pagenow && 'create-course' === Input::get( 'page' );
1428 $backend_edit = $backend_builder && $course_id;
1429
1430 $is_frontend_builder = tutor_utils()->is_tutor_frontend_dashboard( 'create-course' );
1431 $frontend_edit = $is_frontend_builder && $course_id;
1432
1433 if ( ! $backend_edit && ! $frontend_edit ) {
1434 return $data;
1435 }
1436
1437 /**
1438 * Prepare course builder data.
1439 */
1440 $default_data = ( new Assets( false ) )->get_default_localized_data();
1441
1442 if ( isset( $default_data['current_user']['data']['id'] ) ) {
1443 $tutor_user = tutor_utils()->get_tutor_user( $default_data['current_user']['data']['id'] );
1444 $default_data['current_user']['data']['tutor_profile_photo_url'] = $tutor_user->tutor_profile_photo_url;
1445 }
1446
1447 /**
1448 * Localized only options to protect sensitive info like API keys.
1449 */
1450 $required_options = array(
1451 'monetize_by',
1452 'enable_course_marketplace',
1453 'course_permalink_base',
1454 'supported_video_sources',
1455 'enrollment_expiry_enabled',
1456 'enable_q_and_a_on_course',
1457 'instructor_can_delete_course',
1458 'chatgpt_enable',
1459 'hide_admin_bar_for_users',
1460 'enable_redirect_on_course_publish_from_frontend',
1461 'instructor_can_publish_course',
1462 'instructor_can_change_course_author',
1463 'instructor_can_manage_co_instructors',
1464 );
1465
1466 $full_settings = get_option( 'tutor_option', array() );
1467 $settings = Options_V2::get_only( $required_options );
1468 $settings['course_builder_logo_url'] = wp_get_attachment_image_url( $full_settings['tutor_frontend_course_page_logo_id'] ?? 0, 'full' );
1469 $settings['chatgpt_key_exist'] = tutor()->has_pro && ! empty( $full_settings['chatgpt_api_key'] ?? '' );
1470 $settings['youtube_api_key_exist'] = ! empty( $full_settings['lesson_video_duration_youtube_api_key'] ?? '' );
1471
1472 $new_data = array( 'settings' => $settings );
1473
1474 $data = array_merge( $default_data, $new_data );
1475
1476 /**
1477 * Course builder dashboard URL based on role and settings.
1478 */
1479 $dashboard_url = tutor_utils()->tutor_dashboard_url();
1480 if ( User::is_admin() ) {
1481 $dashboard_url = get_admin_url();
1482 }
1483
1484 /**
1485 * EDD product list
1486 */
1487 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1488 if ( 'edd' === $monetize_by && tutor_utils()->has_edd() ) {
1489 $data['edd_products'] = tutor_utils()->get_edd_products();
1490 }
1491
1492 $difficulty_levels = array();
1493 foreach ( tutor_utils()->course_levels() as $value => $label ) {
1494 $difficulty_levels[] = array(
1495 'label' => $label,
1496 'value' => $value,
1497 );
1498 }
1499
1500 $supported_video_sources = array();
1501 $saved_video_source_list = (array) ( $settings['supported_video_sources'] ?? array() );
1502
1503 foreach ( tutor_utils()->get_video_sources( true ) as $value => $label ) {
1504 if ( in_array( $value, $saved_video_source_list, true ) ) {
1505 $supported_video_sources[] = array(
1506 'label' => $label,
1507 'value' => $value,
1508 );
1509 }
1510 }
1511
1512 $data['dashboard_url'] = $dashboard_url;
1513 $data['backend_course_list_url'] = get_admin_url( null, 'admin.php?page=tutor' );
1514 $data['frontend_course_list_url'] = tutor_utils()->tutor_dashboard_url( 'my-courses' );
1515 $data['timezones'] = tutor_global_timezone_lists();
1516 $data['difficulty_levels'] = $difficulty_levels;
1517 $data['supported_video_sources'] = $supported_video_sources;
1518 $data['wp_rest_nonce'] = wp_create_nonce( 'wp_rest' );
1519 $data['max_upload_size'] = size_format( wp_max_upload_size() );
1520
1521 $data = apply_filters( 'tutor_course_builder_localized_data', $data );
1522
1523 return $data;
1524 }
1525
1526 /**
1527 * Load view for course builder.
1528 *
1529 * @since 3.0.0
1530 *
1531 * @return void
1532 */
1533 public function load_course_builder_view() {
1534 /**
1535 * Hide admin menu and footer.
1536 *
1537 * @since 3.3.0
1538 */
1539 echo '<style>
1540 #adminmenumain, #wpfooter, .notice, #tutor-page-wrap { display: none !important; }
1541 #wpcontent { margin: 0 !important; padding: 0 !important; }
1542 #wpbody-content { padding-bottom: 0px !important; float: none; }
1543 </style>';
1544
1545 do_action( 'tutor_before_course_builder_load' );
1546 include_once tutor()->path . 'views/pages/course-builder.php';
1547 do_action( 'tutor_after_course_builder_load' );
1548 }
1549
1550 /**
1551 * Add enroll require login class
1552 *
1553 * @since 2.6.0
1554 *
1555 * @param string $class_name css class name.
1556 *
1557 * @return string
1558 */
1559 public function add_enroll_required_login_class( $class_name ) {
1560 $enabled_tutor_login = tutor_utils()->get_option( 'enable_tutor_native_login', null, true, true );
1561 if ( ! $enabled_tutor_login ) {
1562 return '';
1563 }
1564
1565 return $class_name;
1566 }
1567
1568 /**
1569 * Get list of WC products.
1570 *
1571 * @since 2.5.0
1572 * @since 3.0.0 exclude_linked_products, course_id are added.
1573 *
1574 * @return void
1575 */
1576 public function get_wc_products() {
1577 $exclude = array();
1578 $exclude_linked_products = Input::has( 'exclude_linked_products' );
1579 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1580
1581 if ( $exclude_linked_products ) {
1582 $exclude = tutor_utils()->get_linked_product_ids();
1583 }
1584
1585 if ( $course_id ) {
1586 $linked_product_id = tutor_utils()->get_course_product_id( $course_id );
1587 if ( $linked_product_id ) {
1588 $exclude = array_filter( $exclude, fn( $id )=> $linked_product_id !== (int) $id );
1589 }
1590 }
1591
1592 $exclude = array_unique( $exclude );
1593
1594 $this->json_response(
1595 __( 'Products retrieved successfully!', 'tutor' ),
1596 tutor_utils()->get_wc_products_db( $exclude ),
1597 HttpHelper::STATUS_OK
1598 );
1599 }
1600
1601 /**
1602 * Get course associate WC product info by Ajax request
1603 *
1604 * @since 2.0.7
1605 *
1606 * @return void
1607 */
1608 public function get_wc_product() {
1609 tutor_utils()->checking_nonce();
1610 $product_id = Input::post( 'product_id' );
1611 $product = wc_get_product( $product_id );
1612 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1613
1614 $is_linked_with_course = tutor_utils()->product_belongs_with_course( $product_id );
1615
1616 /**
1617 * If selected product is already linked with
1618 * a course & it is not the current course the
1619 * return error
1620 *
1621 * @since 2.1.0
1622 */
1623 if ( is_object( $is_linked_with_course ) && $is_linked_with_course->post_id != $course_id ) {
1624 wp_send_json_error(
1625 __( 'One product can not be added to multiple course!', 'tutor' )
1626 );
1627 }
1628
1629 if ( $product ) {
1630 $data = array(
1631 'name' => $product->get_name(),
1632 'regular_price' => $product->get_regular_price(),
1633 'sale_price' => $product->get_sale_price(),
1634 );
1635 wp_send_json_success( $data );
1636 } else {
1637 wp_send_json_error( __( 'Product not found', 'tutor' ) );
1638 }
1639 }
1640
1641 /**
1642 * Update course content order
1643 *
1644 * @since 1.0.0
1645 * @return void
1646 */
1647 public function tutor_update_course_content_order() {
1648 tutor_utils()->checking_nonce();
1649
1650 if ( Input::has( 'content_parent' ) ) {
1651 $content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
1652 $topic_id = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
1653 $content_id = tutor_utils()->array_get( 'content_id', $content_parent );
1654
1655 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1656 wp_send_json_success( array( 'message' => __( 'Access Denied!', 'tutor' ) ) );
1657 exit;
1658 }
1659
1660 // Update the parent topic id of the content.
1661 global $wpdb;
1662 $wpdb->update( $wpdb->posts, array( 'post_parent' => $topic_id ), array( 'ID' => $content_id ) );
1663 }
1664
1665 // Save course content order.
1666 $this->save_course_content_order();
1667
1668 wp_send_json_success();
1669 }
1670
1671 /**
1672 * Restrict new student entry
1673 *
1674 * @since 1.0.0
1675 * @param mixed $content content.
1676 *
1677 * @return mixed
1678 */
1679 public function restrict_new_student_entry( $content ) {
1680
1681 if ( ! tutor_utils()->is_course_fully_booked() ) {
1682 // No restriction if not fully booked.
1683 return $content;
1684 }
1685
1686 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">' .
1687 __( 'Fully Booked', 'tutor' )
1688 . '</div></div>';
1689 }
1690
1691 /**
1692 * Restrict media
1693 *
1694 * @since 1.0.0
1695 * @param string $where where clause.
1696 * @return string
1697 */
1698 public function restrict_media( $where ) {
1699 $action = Input::post( 'action' );
1700 if ( 'query-attachments' === $action && tutor_utils()->is_instructor() ) {
1701 if ( ! tutor_utils()->has_user_role( array( 'administrator', 'editor' ) ) ) {
1702 $where .= ' AND post_author=' . get_current_user_id();
1703 }
1704 }
1705
1706 return $where;
1707 }
1708
1709 /**
1710 * Save course content order
1711 *
1712 * @since 1.0.0
1713 * @return void
1714 */
1715 private function save_course_content_order() {
1716 global $wpdb;
1717
1718 $new_order = Input::post( 'tutor_topics_lessons_sorting' );
1719 if ( ! empty( $new_order ) ) {
1720 $order = json_decode( $new_order, true );
1721
1722 if ( is_array( $order ) && count( $order ) ) {
1723 $i = 0;
1724 foreach ( $order as $topic ) {
1725 $i++;
1726 $wpdb->update(
1727 $wpdb->posts,
1728 array( 'menu_order' => $i ),
1729 array( 'ID' => $topic['topic_id'] )
1730 );
1731
1732 /**
1733 * Removing All lesson with topic
1734 */
1735
1736 $wpdb->update(
1737 $wpdb->posts,
1738 array( 'post_parent' => 0 ),
1739 array( 'post_parent' => $topic['topic_id'] )
1740 );
1741
1742 /**
1743 * Lesson Attaching with topic ID
1744 * Sorting lesson
1745 */
1746 if ( isset( $topic['lesson_ids'] ) ) {
1747 $lesson_ids = $topic['lesson_ids'];
1748 } else {
1749 $lesson_ids = array();
1750 }
1751 if ( count( $lesson_ids ) ) {
1752 foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
1753 $wpdb->update(
1754 $wpdb->posts,
1755 array(
1756 'post_parent' => $topic['topic_id'],
1757 'menu_order' => $lesson_key,
1758 ),
1759 array( 'ID' => $lesson_id )
1760 );
1761 }
1762 }
1763 }
1764 }
1765 }
1766 }
1767
1768 /**
1769 * Insert Topic and attached it with Course
1770 *
1771 * @since 1.0.0
1772 *
1773 * @param integer $post_ID post ID.
1774 * @param object $post post object.
1775 *
1776 * @return void
1777 */
1778 public function save_course_meta( $post_ID, $post ) {
1779 global $wpdb;
1780
1781 do_action( 'tutor_save_course', $post_ID, $post );
1782
1783 /**
1784 * Save course price type
1785 */
1786 $price_type = Input::post( 'tutor_course_price_type' );
1787 if ( $price_type ) {
1788 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, $price_type );
1789 }
1790
1791 //phpcs:disable WordPress.Security.NonceVerification.Missing
1792 // Course Duration.
1793 if ( ! empty( $_POST['course_duration'] ) ) {
1794 $video = Input::post( 'course_duration', array(), Input::TYPE_ARRAY );
1795 update_post_meta( $post_ID, '_course_duration', $video );
1796 }
1797
1798 if ( ! empty( $_POST['_tutor_course_level'] ) ) {
1799 $course_level = Input::post( '_tutor_course_level' );
1800 update_post_meta( $post_ID, '_tutor_course_level', $course_level );
1801 }
1802
1803 $additional_data_edit = Input::post( '_tutor_course_additional_data_edit' );
1804 if ( $additional_data_edit ) {
1805 if ( ! empty( $_POST['course_benefits'] ) ) {
1806 $course_benefits = Input::post( 'course_benefits', '', Input::TYPE_KSES_POST );
1807 update_post_meta( $post_ID, '_tutor_course_benefits', $course_benefits );
1808 } elseif ( ! tutor_is_rest() ) {
1809 delete_post_meta( $post_ID, '_tutor_course_benefits' );
1810 }
1811
1812 if ( ! empty( $_POST['course_requirements'] ) ) {
1813 $requirements = Input::post( 'course_requirements', '', Input::TYPE_KSES_POST );
1814 update_post_meta( $post_ID, '_tutor_course_requirements', $requirements );
1815 } elseif ( ! tutor_is_rest() ) {
1816 delete_post_meta( $post_ID, '_tutor_course_requirements' );
1817 }
1818
1819 if ( ! empty( $_POST['course_target_audience'] ) ) {
1820 $target_audience = Input::post( 'course_target_audience', '', Input::TYPE_KSES_POST );
1821 update_post_meta( $post_ID, '_tutor_course_target_audience', $target_audience );
1822 } elseif ( ! tutor_is_rest() ) {
1823 delete_post_meta( $post_ID, '_tutor_course_target_audience' );
1824 }
1825
1826 if ( ! empty( $_POST['course_material_includes'] ) ) {
1827 $material_includes = Input::post( 'course_material_includes', '', Input::TYPE_KSES_POST );
1828 update_post_meta( $post_ID, '_tutor_course_material_includes', $material_includes );
1829 } elseif ( ! tutor_is_rest() ) {
1830 delete_post_meta( $post_ID, '_tutor_course_material_includes' );
1831 }
1832 //phpcs:enable WordPress.Security.NonceVerification.Missing
1833 }
1834
1835 /**
1836 * Sorting Topics and lesson
1837 */
1838 $this->save_course_content_order();
1839
1840 // Additional data like course intro video.
1841 if ( $additional_data_edit ) {
1842 // Sanitize data through helper method.
1843 $video = Input::sanitize_array(
1844 $_POST['video'] ?? array(), //phpcs:ignore
1845 array(
1846 'source_external_url' => 'esc_url',
1847 'source_embedded' => 'wp_kses_post',
1848 ),
1849 true
1850 );
1851 $video_source = tutor_utils()->array_get( 'source', $video );
1852 if ( -1 !== $video_source ) {
1853 update_post_meta( $post_ID, '_video', $video );
1854 } elseif ( ! tutor_is_rest() ) {
1855 delete_post_meta( $post_ID, '_video' );
1856 }
1857 }
1858
1859 /**
1860 * Adding author to instructor automatically
1861 */
1862
1863 // Override post author id.
1864 $author_id = isset( $_POST['post_author_override'] ) ? $_POST['post_author_override'] : $post->post_author; //phpcs:ignore
1865 $attached = (int) $wpdb->get_var(
1866 $wpdb->prepare(
1867 "SELECT COUNT(umeta_id) FROM {$wpdb->usermeta}
1868 WHERE user_id = %d
1869 AND meta_key = '_tutor_instructor_course_id'
1870 AND meta_value = %d ",
1871 $author_id,
1872 $post_ID
1873 )
1874 );
1875
1876 if ( ! $attached ) {
1877 add_user_meta( $author_id, '_tutor_instructor_course_id', $post_ID );
1878 }
1879
1880 /**
1881 * Disable question and answer for this course
1882 *
1883 * @since 1.7.0
1884 */
1885 if ( $additional_data_edit ) {
1886 foreach ( $this->additional_meta as $key ) {
1887 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1888 update_post_meta( $post_ID, $key, ( isset( $_POST[ $key ] ) ? 'yes' : 'no' ) );
1889 }
1890 }
1891
1892 do_action( 'tutor_save_course_after', $post_ID, $post );
1893 }
1894
1895 /**
1896 * Save course topic
1897 *
1898 * @since 1.0.0
1899 * @since 3.0.0 response and input name updated.
1900 *
1901 * @return void
1902 */
1903 public function tutor_save_topic() {
1904 tutor_utils()->check_nonce();
1905
1906 $is_update = false;
1907 $errors = array();
1908 $topic_title = Input::post( 'title' );
1909
1910 if ( empty( $topic_title ) ) {
1911 $errors['topic_title'] = __( 'Topic title is required!', 'tutor' );
1912 $this->json_response(
1913 __( 'Invalid inputs', 'tutor' ),
1914 $errors,
1915 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
1916 );
1917 }
1918
1919 // Gather parameters.
1920 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1921 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1922 $topic_summary = Input::post( 'summary', '', Input::TYPE_KSES_POST );
1923
1924 $next_topic_order_id = tutor_utils()->get_next_topic_order_id( $course_id, $topic_id );
1925
1926 // Validate if user can manage the topic.
1927 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) || ( $topic_id && ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) ) {
1928 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1929 }
1930
1931 // Create payload to create/update the topic.
1932 $post_arr = array(
1933 'post_type' => 'topics',
1934 'post_title' => $topic_title,
1935 'post_content' => $topic_summary,
1936 'post_status' => 'publish',
1937 'post_author' => get_current_user_id(),
1938 'post_parent' => $course_id,
1939 'menu_order' => $next_topic_order_id,
1940 );
1941
1942 if ( $topic_id ) {
1943 $is_update = true;
1944 $post_arr['ID'] = $topic_id;
1945 }
1946
1947 $current_topic_id = wp_insert_post( $post_arr );
1948
1949 if ( $is_update ) {
1950 $this->json_response(
1951 __( 'Topic updated successfully!', 'tutor' ),
1952 $current_topic_id
1953 );
1954 } else {
1955 $this->json_response(
1956 __( 'Topic created successfully!', 'tutor' ),
1957 $current_topic_id,
1958 HttpHelper::STATUS_CREATED
1959 );
1960 }
1961 }
1962
1963 /**
1964 * Delete a course topic
1965 *
1966 * @since 1.0.0
1967 * @since 3.0.0 code refactor and response updated.
1968 *
1969 * @return void
1970 */
1971 public function tutor_delete_topic() {
1972 tutor_utils()->check_nonce();
1973
1974 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1975 if ( ! $topic_id || ! is_numeric( $topic_id ) || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1976 $this->json_response(
1977 tutor_utils()->error_message(),
1978 null,
1979 HttpHelper::STATUS_FORBIDDEN
1980 );
1981 }
1982
1983 global $wpdb;
1984
1985 // Assign course ID to orphan content IDs since the topic will be deleted.
1986 $course_id = tutor_utils()->get_course_id_by( 'topic', $topic_id );
1987 $content_ids = tutor_utils()->get_course_content_ids_by( null, 'topic', $topic_id );
1988 foreach ( $content_ids as $content_id ) {
1989 update_post_meta( $content_id, '_tutor_course_id_for_lesson', $course_id );
1990 // Actually all kind of contents.
1991 // This keyword '_tutor_course_id_for_lesson' used just to support backward compatibility.
1992 }
1993
1994 // Set contents under the topic orphan.
1995 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'post_parent' => $topic_id ) );
1996
1997 // Then delete the topic from database.
1998 $wpdb->delete( $wpdb->postmeta, array( 'post_id' => $topic_id ) );
1999 wp_delete_post( $topic_id );
2000
2001 $this->json_response(
2002 __( 'Topic deleted successfully!', 'tutor' )
2003 );
2004 }
2005
2006 /**
2007 * Handle enroll now action
2008 *
2009 * @since 1.0.0
2010 *
2011 * @return void
2012 */
2013 public function enroll_now() {
2014
2015 if ( '_tutor_course_enroll_now' !== Input::post( 'tutor_course_action' ) || ! Input::has( 'tutor_course_id' ) ) {
2016 return;
2017 }
2018
2019 // Checking Nonce.
2020 tutor_utils()->checking_nonce();
2021
2022 $user_id = get_current_user_id();
2023 if ( ! $user_id ) {
2024 exit( esc_html__( 'Please Sign In first', 'tutor' ) );
2025 }
2026
2027 $course_id = Input::post( 'tutor_course_id', 0, Input::TYPE_INT );
2028
2029 /**
2030 * TODO: need to check purchase information
2031 */
2032
2033 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2034
2035 /**
2036 * If is is not purchasable, it's free, and enroll right now
2037 * If purchasable, then process purchase.
2038 *
2039 * @since: v.1.0.0
2040 */
2041 if ( $is_purchasable ) { //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
2042 // Process purchase.
2043
2044 } else {
2045 // Free enroll.
2046 tutor_utils()->do_enroll( $course_id );
2047 }
2048
2049 $referer_url = wp_get_referer();
2050 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
2051 exit;
2052 }
2053
2054 /**
2055 * Mark complete completed
2056 *
2057 * @since 1.0.0
2058 * @return void
2059 */
2060 public function mark_course_complete() {
2061 $tutor_action = Input::post( 'tutor_action' );
2062 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2063 if ( 'tutor_complete_course' !== $tutor_action || ! $course_id ) {
2064 return;
2065 }
2066
2067 // Checking nonce.
2068 tutor_utils()->checking_nonce();
2069
2070 $user_id = get_current_user_id();
2071
2072 // TODO: need to show view if not signed_in.
2073 if ( ! $user_id ) {
2074 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2075 }
2076
2077 CourseModel::mark_course_as_completed( $course_id, $user_id );
2078
2079 $permalink = get_the_permalink( $course_id );
2080
2081 // Set temporary identifier to show review pop up.
2082 self::set_review_popup_data( $user_id, $course_id, $permalink );
2083
2084 wp_safe_redirect( $permalink );
2085 exit;
2086 }
2087
2088 /**
2089 * Set data for review popup.
2090 *
2091 * @since 2.2.5
2092 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
2093 *
2094 * @param int $user_id user id.
2095 * @param int $course_id course id.
2096 *
2097 * @return void
2098 */
2099 public static function set_review_popup_data( $user_id, $course_id ) {
2100 if ( get_tutor_option( 'enable_course_review' ) ) {
2101 $rating = tutor_utils()->get_course_rating_by_user( $course_id, $user_id );
2102 if ( ! $rating || ( empty( $rating->rating ) && empty( $rating->review ) ) ) {
2103 $meta_key = User::get_review_popup_meta( $course_id );
2104 add_user_meta( $user_id, $meta_key, $course_id, true );
2105 }
2106 }
2107 }
2108
2109 /**
2110 * Popup review form on course details
2111 *
2112 * @since 1.0.0
2113 * @return void
2114 */
2115 public function popup_review_form() {
2116 if ( is_user_logged_in() ) {
2117 $user_id = get_current_user_id();
2118 $course_id = get_the_ID();
2119 $meta_key = User::get_review_popup_meta( $course_id );
2120 $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2121
2122 if ( is_single() && $course_id === $review_course_id ) {
2123 include tutor()->path . 'views/modal/review.php';
2124 }
2125 }
2126 }
2127
2128 /**
2129 * Review popup data clear
2130 *
2131 * @since 2.4.0
2132 *
2133 * @return void
2134 */
2135 public function clear_review_popup_data() {
2136 tutils()->checking_nonce();
2137
2138 if ( is_user_logged_in() ) {
2139 $user_id = get_current_user_id();
2140 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2141
2142 if ( $course_id ) {
2143 $meta_key = User::get_review_popup_meta( $course_id );
2144 delete_user_meta( $user_id, $meta_key, $course_id );
2145 }
2146
2147 wp_send_json_success();
2148 }
2149 }
2150
2151 /**
2152 * Delete course delete from frontend dashboard
2153 *
2154 * @since 2.0.0
2155 * @return void
2156 */
2157 public function tutor_delete_dashboard_course() {
2158 tutor_utils()->checking_nonce();
2159
2160 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2161 if ( ! tutor_utils()->can_user_manage( 'course', $course_id ) ) {
2162 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2163 }
2164
2165 /**
2166 * Co-instructor can not delete a course
2167 *
2168 * @since 2.1.6
2169 */
2170 if ( false === CourseModel::is_main_instructor( $course_id ) ) {
2171 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2172 }
2173
2174 // Check if user is only an instructor.
2175 if ( ! current_user_can( 'administrator' ) ) {
2176 // Check if instructor can trash course.
2177 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2178
2179 if ( ! $can_trash_post ) {
2180 wp_send_json_error( tutor_utils()->error_message() );
2181 }
2182 }
2183
2184 $trash_course = wp_update_post(
2185 array(
2186 'ID' => $course_id,
2187 'post_status' => 'trash',
2188 )
2189 );
2190
2191 if ( $trash_course ) {
2192 wp_send_json_success( __( 'Course has been trashed successfully ', 'tutor' ) );
2193 }
2194 wp_send_json_success();
2195 }
2196
2197 /**
2198 * Main author change from gutenberg editor
2199 *
2200 * @since 2.0.0
2201 *
2202 * @param array $data data.
2203 * @param array $postarr post array.
2204 *
2205 * @return mixed
2206 */
2207 public function tutor_add_gutenberg_author( $data, $postarr ) {
2208 $gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2209 $post_type = $postarr['post_type'];
2210 $courses_post_type = tutor()->course_post_type;
2211
2212 if ( false === is_admin() || false === $gutenberg_enabled || $post_type !== $courses_post_type ) {
2213 return $data;
2214 }
2215
2216 /**
2217 * Only admin can change main author
2218 */
2219 if ( $courses_post_type === $post_type && ! current_user_can( 'administrator' ) ) {
2220 global $wpdb;
2221 $post_ID = (int) tutor_utils()->avalue_dot( 'ID', $postarr );
2222 $post_author = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_author FROM {$wpdb->posts} WHERE ID = %d ", $post_ID ) );
2223
2224 if ( $post_author > 0 ) {
2225 $data['post_author'] = $post_author;
2226 } else {
2227 $data['post_author'] = get_current_user_id();
2228 }
2229 }
2230
2231 return $data;
2232 }
2233
2234
2235 /**
2236 * Attach product with course when course save from frontend or backend.
2237 *
2238 * @since 1.3.4
2239 *
2240 * @since 3.0.0 Store regular & sale price in meta to make compatible with Tutor monetization
2241 *
2242 * @param integer $post_ID course ID.
2243 * @param array $post_data created course post details.
2244 *
2245 * @return void
2246 */
2247 public function attach_product_with_course( $post_ID, $post_data ) {
2248 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
2249 $product_id = Input::post( '_tutor_course_product_id', 0, Input::TYPE_INT );
2250
2251 /**
2252 * For native monetization, just return
2253 * No need to attach anything.
2254 */
2255 if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
2256 return;
2257 }
2258
2259 /**
2260 * When course moved paid to free
2261 * Keep the product linked and return.
2262 */
2263 if ( -1 === $product_id ) {
2264 return;
2265 }
2266
2267 /**
2268 * Free user can only select product from dropdown
2269 */
2270 if ( tutor()->has_pro === false && 'wc' === $monetize_by ) {
2271 if ( $product_id > 0 ) {
2272 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2273 }
2274
2275 return;
2276 }
2277
2278 $attached_product_id = tutor_utils()->get_course_product_id( $post_ID );
2279 $course_price = Input::post( 'course_price', 0, Input::TYPE_NUMERIC );
2280 $sale_price = Input::post( 'course_sale_price', 0, Input::TYPE_NUMERIC );
2281
2282 if ( ! $course_price || $sale_price >= $course_price ) {
2283 return;
2284 }
2285
2286 $course = get_post( $post_ID );
2287
2288 update_post_meta( $post_ID, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_PAID );
2289
2290 if ( 'wc' === $monetize_by ) {
2291
2292 $is_update = ( $product_id && wc_get_product( $product_id ) ) ? true : false;
2293
2294 if ( $is_update ) {
2295 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2296
2297 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price, $product_id );
2298 $product_obj = wc_get_product( $product_id );
2299 if ( $product_obj->is_type( 'subscription' ) ) {
2300 update_post_meta( $product_id, '_subscription_price', $course_price );
2301 }
2302
2303 // Set course regular & sale price.
2304 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2305 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2306 } else {
2307 // Create new WC product name with course title.
2308 $product_id = self::create_wc_product( $course->post_title, $course_price, $sale_price );
2309 if ( $product_id ) {
2310 $product_obj = wc_get_product( $product_id );
2311 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $product_id );
2312 // Mark product for woocommerce.
2313 update_post_meta( $product_id, '_virtual', 'yes' );
2314 update_post_meta( $product_id, '_tutor_product', 'yes' );
2315
2316 // Set course regular & sale price.
2317 update_post_meta( $post_ID, self::COURSE_PRICE_META, $product_obj->get_regular_price() );
2318 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $product_obj->get_sale_price() );
2319 }
2320 }
2321
2322 $course_post_thumbnail = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
2323 if ( $product_id && $course_post_thumbnail ) {
2324 set_post_thumbnail( $product_id, $course_post_thumbnail );
2325 }
2326 } elseif ( 'edd' === $monetize_by ) {
2327
2328 $is_update = false;
2329
2330 if ( $attached_product_id ) {
2331 $edd_price = get_post_meta( $attached_product_id, 'edd_price', true );
2332 if ( $edd_price ) {
2333 $is_update = true;
2334 }
2335 }
2336
2337 if ( $is_update ) {
2338 // Update the product.
2339 update_post_meta( $attached_product_id, 'edd_price', $course_price );
2340 } else {
2341 // Create new product.
2342
2343 $post_arr = array(
2344 'post_type' => 'download',
2345 'post_title' => $course->post_title,
2346 'post_status' => 'publish',
2347 'post_author' => get_current_user_id(),
2348 );
2349 $download_id = wp_insert_post( $post_arr );
2350 if ( $download_id ) {
2351 // EDD edd_price.
2352 update_post_meta( $download_id, 'edd_price', $course_price );
2353
2354 update_post_meta( $post_ID, self::COURSE_PRODUCT_ID_META, $download_id );
2355 // Mark product for EDD.
2356 update_post_meta( $download_id, '_tutor_product', 'yes' );
2357
2358 $course_post_thumbnail = get_post_meta( $post_ID, '_thumbnail_id', true );
2359 if ( $course_post_thumbnail ) {
2360 set_post_thumbnail( $download_id, $course_post_thumbnail );
2361 }
2362 }
2363 }
2364 }
2365 }
2366
2367 /**
2368 * Add Course level to course settings
2369 *
2370 * @since 1.4.1
2371 *
2372 * @param array $args arguments.
2373 * @return array
2374 */
2375 public function add_course_level_to_settings( $args ) {
2376 $course_id = get_the_ID();
2377 $levels = tutor_utils()->course_levels();
2378 $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2379
2380 $args['general']['fields']['_tutor_course_level'] = array(
2381 'type' => 'select',
2382 'label' => __( 'Difficulty Level', 'tutor' ),
2383 'label_title' => __( 'Enable', 'tutor' ),
2384 'options' => $levels,
2385 'value' => $course_level ? $course_level : 'intermediate',
2386 'desc' => __( 'Course difficulty level', 'tutor' ),
2387 );
2388
2389 return $args;
2390 }
2391
2392 /**
2393 * Check if course starting
2394 *
2395 * @since 1.4.8
2396 * @return void
2397 */
2398 public function tutor_lesson_load_before() {
2399 $course_id = tutor_utils()->get_course_id_by_content( get_the_ID() );
2400 $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id );
2401 if ( is_user_logged_in() ) {
2402 $is_course_started = get_post_meta( $course_id, '_tutor_course_started', true );
2403 if ( ! $completed_lessons && ! $is_course_started ) {
2404 update_post_meta( $course_id, '_tutor_course_started', tutor_time() );
2405 do_action( 'tutor/course/started', $course_id );
2406 }
2407 }
2408 }
2409
2410 /**
2411 * Add Course level to course settings
2412 *
2413 * @since 1.4.8
2414 * @return void
2415 */
2416 public function course_elements_enable_disable() {
2417 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
2418 add_filter( 'tutor_course/single/material_includes', array( $this, 'enable_disable_material_includes' ) );
2419 add_filter( 'tutor_course/single/content', array( $this, 'enable_disable_course_content' ) );
2420 add_filter( 'tutor_course/single/benefits_html', array( $this, 'enable_disable_course_benefits' ) );
2421 add_filter( 'tutor_course/single/requirements_html', array( $this, 'enable_disable_course_requirements' ) );
2422 add_filter( 'tutor_course/single/audience_html', array( $this, 'enable_disable_course_target_audience' ) );
2423 add_filter( 'tutor_course/single/nav_items', array( $this, 'enable_disable_course_nav_items' ), 999, 2 );
2424 }
2425
2426 /**
2427 * Enable disable course progress bar
2428 *
2429 * @since 1.4.8
2430 *
2431 * @param string $html HTML string.
2432 * @return string
2433 */
2434 public function enable_disable_course_progress_bar( $html ) {
2435 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_progress_bar', true, true );
2436 if ( $disable_option ) {
2437 return '';
2438 }
2439 return $html;
2440 }
2441
2442 /**
2443 * Enable disable material includes
2444 *
2445 * @since 1.4.8
2446 *
2447 * @param string $html HTML string.
2448 * @return string
2449 */
2450 public function enable_disable_material_includes( $html ) {
2451 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
2452 if ( $disable_option ) {
2453 return '';
2454 }
2455 return $html;
2456 }
2457
2458 /**
2459 * Enable disable course content
2460 *
2461 * @since 1.4.8
2462 *
2463 * @param string $html HTML string.
2464 * @return string
2465 */
2466 public function enable_disable_course_content( $html ) {
2467 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
2468 if ( $disable_option ) {
2469 return '';
2470 }
2471 return $html;
2472 }
2473
2474 /**
2475 * Enable disable course benefits
2476 *
2477 * @since 1.4.8
2478 *
2479 * @param string $html HTML string.
2480 * @return string
2481 */
2482 public function enable_disable_course_benefits( $html ) {
2483 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
2484 if ( $disable_option ) {
2485 return '';
2486 }
2487 return $html;
2488 }
2489
2490 /**
2491 * Enable disable course requirements
2492 *
2493 * @since 1.4.8
2494 *
2495 * @param string $html HTML string.
2496 * @return string
2497 */
2498 public function enable_disable_course_requirements( $html ) {
2499 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
2500 if ( $disable_option ) {
2501 return '';
2502 }
2503 return $html;
2504 }
2505
2506 /**
2507 * Enable disable course target audience
2508 *
2509 * @since 1.4.8
2510 *
2511 * @param string $html HTML string.
2512 * @return string
2513 */
2514 public function enable_disable_course_target_audience( $html ) {
2515 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
2516 if ( $disable_option ) {
2517 return '';
2518 }
2519 return $html;
2520 }
2521
2522 /**
2523 * Enable disable course nav items
2524 *
2525 * @since 1.4.8
2526 *
2527 * @param array $items item list.
2528 * @param integer $course_id course ID.
2529 *
2530 * @return array
2531 */
2532 public function enable_disable_course_nav_items( $items, $course_id ) {
2533 global $wp_query, $post;
2534 $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' );
2535 $disable_course_announcements = ! (bool) tutor_utils()->get_option( 'enable_course_announcements', true, true );
2536 $disable_qa_for_this_course = ( $wp_query->is_single && ! empty( $post ) ) ? get_post_meta( $post->ID, '_tutor_enable_qa', true ) != 'yes' : false;
2537
2538 // Whether Q&A enabled.
2539 if ( ! $enable_q_and_a_on_course || $disable_qa_for_this_course ) {
2540 if ( tutor_utils()->array_get( 'questions', $items ) ) {
2541 unset( $items['questions'] );
2542 }
2543 }
2544
2545 // Whether announcment enabled.
2546 if ( $disable_course_announcements ) {
2547 if ( tutor_utils()->array_get( 'announcements', $items ) ) {
2548 unset( $items['announcements'] );
2549 }
2550 }
2551
2552 // Hide review section if disabled.
2553 if ( ! get_tutor_option( 'enable_course_review' ) ) {
2554 unset( $items['reviews'] );
2555 }
2556
2557 // Whether enrollment require.
2558 $is_enrolled = tutor_utils()->is_enrolled();
2559
2560 return array_filter(
2561 $items,
2562 function ( $item ) use ( $is_enrolled ) {
2563 if ( isset( $item['require_enrolment'] ) && $item['require_enrolment'] ) {
2564 return $is_enrolled;
2565 }
2566 return true;
2567 }
2568 );
2569 }
2570
2571 /**
2572 * Filter product in shop page
2573 *
2574 * @since 1.4.9
2575 * @return void|null
2576 */
2577 public function filter_product_in_shop_page() {
2578 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
2579 if ( ! $hide_course_from_shop_page ) {
2580 return;
2581 }
2582 add_action( 'woocommerce_product_query', array( $this, 'filter_woocommerce_product_query' ) );
2583 add_filter( 'edd_downloads_query', array( $this, 'filter_edd_downloads_query' ), 10, 2 );
2584 add_action( 'pre_get_posts', array( $this, 'filter_archive_meta_query' ), 1 );
2585 }
2586
2587
2588 /**
2589 * Tutor product meta query
2590 *
2591 * @since 1.4.9
2592 * @return array
2593 */
2594 public function tutor_product_meta_query() {
2595 $meta_query = array(
2596 'key' => '_tutor_product',
2597 'compare' => 'NOT EXISTS',
2598 );
2599 return $meta_query;
2600 }
2601
2602 /**
2603 * Filter product in woocommerce shop page
2604 *
2605 * @since 1.4.9
2606 *
2607 * @param \WP_Query $wp_query WP Query instance.
2608 * @return \WP_Query
2609 */
2610 public function filter_woocommerce_product_query( $wp_query ) {
2611 $product_ids = $this->get_connected_wc_product_ids();
2612 $wp_query->set( 'post__not_in', $product_ids );
2613 return $wp_query;
2614 }
2615
2616 /**
2617 * Get connected woocommerce product ids for course and course bundle
2618 *
2619 * @since 2.7.2
2620 *
2621 * @return array
2622 */
2623 public function get_connected_wc_product_ids() {
2624 global $wpdb;
2625
2626 $results = $wpdb->get_results(
2627 $wpdb->prepare(
2628 "SELECT DISTINCT pm.meta_value product_id
2629 FROM {$wpdb->posts} p
2630 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
2631 AND pm.meta_key = %s
2632 WHERE post_type IN( 'courses','course-bundle' )",
2633 '_tutor_course_product_id'
2634 )
2635 );
2636
2637 $ids = array();
2638 if ( is_array( $results ) && count( $results ) ) {
2639 $ids = array_column( $results, 'product_id' );
2640 }
2641
2642 return $ids;
2643 }
2644
2645 /**
2646 * Filter product in edd downloads shortcode page
2647 *
2648 * @since 1.4.9
2649 *
2650 * @param \WP_Query $query WP Query instance.
2651 * @return \WP_Query
2652 */
2653 public function filter_edd_downloads_query( $query ) {
2654 $query['meta_query'][] = $this->tutor_product_meta_query();
2655 return $query;
2656 }
2657
2658 /**
2659 * Filter product in edd downloads archive page
2660 *
2661 * @since 1.4.9
2662 *
2663 * @param \WP_Query $wp_query WP Query instance.
2664 * @return \WP_Query
2665 */
2666 public function filter_archive_meta_query( $wp_query ) {
2667 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
2668 $wp_query->set( 'meta_query', array( $this->tutor_product_meta_query() ) );
2669 }
2670 return $wp_query;
2671 }
2672
2673 /**
2674 * Removed course price if already enrolled at single course
2675 *
2676 * @since 1.5.8
2677 *
2678 * @param string $html HTML string.
2679 * @return string
2680 */
2681 public function remove_price_if_enrolled( $html ) {
2682 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2683
2684 if ( $should_removed ) {
2685 $course_id = get_the_ID();
2686 $enrolled = tutor_utils()->is_enrolled( $course_id );
2687 if ( $enrolled ) {
2688 $html = '';
2689 }
2690 }
2691 return $html;
2692 }
2693
2694 /**
2695 * Check if all lessons and quizzes done before mark course complete.
2696 *
2697 * @since 1.5.8
2698 *
2699 * @param string $html HTML string.
2700 * @return string
2701 */
2702 public function tutor_lms_hide_course_complete_btn( $html ) {
2703
2704 $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2705 if ( 'strict' !== $completion_mode ) {
2706 return $html;
2707 }
2708
2709 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2710 $lesson_count = tutor_utils()->get_lesson_count_by_course();
2711
2712 if ( $completed_lesson < $lesson_count ) {
2713 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2714 <div class="tutor-alert-text">
2715 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2716 <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2717 </div>
2718 </div>';
2719 }
2720
2721 $quizzes = array();
2722 $assignments = array();
2723
2724 $course_contents = tutor_utils()->get_course_contents_by_id();
2725 if ( tutor_utils()->count( $course_contents ) ) {
2726 foreach ( $course_contents as $content ) {
2727 if ( 'tutor_quiz' === $content->post_type ) {
2728 $quizzes[] = $content;
2729 }
2730 if ( 'tutor_assignments' === $content->post_type ) {
2731 $assignments[] = $content;
2732 }
2733 }
2734 }
2735
2736 $required_assignment_pass = 0;
2737
2738 foreach ( $assignments as $row ) {
2739
2740 $submitted_assignment = tutor_utils()->is_assignment_submitted( $row->ID );
2741 $is_reviewed_by_instructor = null === $submitted_assignment
2742 ? false
2743 : get_comment_meta( $submitted_assignment->comment_ID, 'evaluate_time', true );
2744
2745 if ( $submitted_assignment && $is_reviewed_by_instructor ) {
2746 $pass_mark = tutor_utils()->get_assignment_option( $submitted_assignment->comment_post_ID, 'pass_mark' );
2747 $given_mark = get_comment_meta( $submitted_assignment->comment_ID, 'assignment_mark', true );
2748
2749 if ( $given_mark < $pass_mark ) {
2750 $required_assignment_pass++;
2751 }
2752 } else {
2753 $required_assignment_pass++;
2754 }
2755 }
2756
2757 $is_quiz_pass = true;
2758 $required_quiz_pass = 0;
2759
2760 if ( tutor_utils()->count( $quizzes ) ) {
2761 foreach ( $quizzes as $quiz ) {
2762
2763 $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2764 if ( $attempt ) {
2765 $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2766 $earned_percentage = $attempt->earned_marks > 0 ? ( number_format( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) ) : 0;
2767
2768 if ( $earned_percentage < $passing_grade ) {
2769 $required_quiz_pass++;
2770 $is_quiz_pass = false;
2771 }
2772 } else {
2773 $required_quiz_pass++;
2774 $is_quiz_pass = false;
2775 }
2776 }
2777 }
2778
2779 if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2780 $_msg = '';
2781 $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2782 $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2783
2784 if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2785 /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2786 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2787 }
2788
2789 if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2790 //phpcs:ignore
2791 $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2792 }
2793
2794 if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2795 /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2796 $_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 );
2797 }
2798
2799 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2800 <div class="tutor-alert-text">
2801 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2802 <span>' . $_msg . '</span>
2803 </div>
2804 </div>';
2805 }
2806
2807 return $html;
2808 }
2809
2810 /**
2811 * Generate Gradebook
2812 *
2813 * @since 1.5.8
2814 *
2815 * @param string $html HTML string.
2816 * @return string
2817 */
2818 public function get_generate_greadbook( $html ) {
2819 if ( ! tutor_utils()->is_completed_course() ) {
2820 return '';
2821 }
2822 return $html;
2823 }
2824
2825 /**
2826 * Add social share content in header
2827 *
2828 * @since 1.6.3
2829 * @return void
2830 */
2831 public function social_share_content() {
2832 global $wp_query, $post;
2833 if ( $wp_query->is_single && ! empty( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] === $this->course_post_type ) { ?>
2834 <!--Facebook-->
2835 <meta property="og:type" content="website"/>
2836 <meta property="og:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>" />
2837 <meta property="og:description" content="<?php echo esc_html( $post->post_content ); ?>" />
2838 <!--Twitter-->
2839 <meta name="twitter:image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2840 <meta name="twitter:description" content="<?php echo esc_html( $post->post_content ); ?>">
2841 <!--Google+-->
2842 <meta itemprop="image" content="<?php echo esc_url( get_tutor_course_thumbnail_src() ); ?>">
2843 <meta itemprop="description" content="<?php echo esc_html( $post->post_content ); ?>">
2844 <?php
2845 }
2846 }
2847
2848 /**
2849 * Delete associated enrollment
2850 *
2851 * @since 1.8.2
2852 *
2853 * @param integer $post_id post ID.
2854 * @return void
2855 */
2856 public function delete_associated_enrollment( $post_id ) {
2857 global $wpdb;
2858
2859 $enroll_id = $wpdb->get_var(
2860 $wpdb->prepare(
2861 "SELECT
2862 post_id
2863 FROM
2864 {$wpdb->postmeta}
2865 WHERE
2866 meta_key='_tutor_enrolled_by_order_id'
2867 AND meta_value = %d
2868 ",
2869 $post_id
2870 )
2871 );
2872
2873 if ( is_numeric( $enroll_id ) && $enroll_id > 0 ) {
2874
2875 $course_id = get_post_field( 'post_parent', $enroll_id );
2876 $user_id = get_post_field( 'post_author', $enroll_id );
2877
2878 tutor_utils()->cancel_course_enrol( $course_id, $user_id );
2879 }
2880 }
2881
2882 /**
2883 * Reset course progress.
2884 *
2885 * @since 1.5.8
2886 * @return void
2887 */
2888 public function tutor_reset_course_progress() {
2889 tutor_utils()->checking_nonce();
2890 $course_id = Input::post( 'course_id' );
2891
2892 if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2893 wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
2894 return;
2895 }
2896
2897 tutor_utils()->delete_course_progress( $course_id );
2898 wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2899 }
2900
2901 /**
2902 * Do enroll if guest attempt to enroll and course is free
2903 *
2904 * @since 1.9.8
2905 *
2906 * @param integer $course_id course ID.
2907 * @param integer $user_id user ID.
2908
2909 * @return void
2910 */
2911 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
2912 $course_id = sanitize_text_field( $course_id );
2913 $is_allowed = apply_filters( 'tutor_allow_guest_attempt_enrollment', true, $course_id, $user_id );
2914
2915 if ( $course_id && $is_allowed ) {
2916 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
2917 if ( ! $is_purchasable ) {
2918 tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
2919 do_action( 'guest_attempt_after_enrollment', $course_id );
2920 }
2921 }
2922 }
2923
2924 /**
2925 * Handle course enrollment
2926 *
2927 * @since 2.1.0
2928 * @return void
2929 */
2930 public function course_enrollment() {
2931 tutor_utils()->checking_nonce();
2932
2933 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2934 $user_id = get_current_user_id();
2935
2936 if ( $course_id ) {
2937 $password_protected = post_password_required( $course_id );
2938 if ( $password_protected ) {
2939 wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
2940 }
2941 $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
2942 if ( $enroll ) {
2943 wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
2944 } else {
2945 wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
2946 }
2947 } else {
2948 wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
2949 }
2950 }
2951
2952 /**
2953 * After trash a course direct to the course list page
2954 *
2955 * @since 2.1.7
2956 *
2957 * @param integer $post_id int course id.
2958 *
2959 * @return void
2960 */
2961 public static function redirect_to_course_list_page( int $post_id ): void {
2962 $post = get_post( $post_id );
2963 if ( tutor()->course_post_type === $post->post_type ) {
2964 $is_gutenberg_enabled = tutor_utils()->get_option( 'enable_gutenberg_course_edit' );
2965 if ( ! $is_gutenberg_enabled ) {
2966 wp_safe_redirect( admin_url( 'admin.php?page=tutor' ) );
2967 exit;
2968 }
2969 }
2970 }
2971
2972 /**
2973 * Create or update WooCommerce product
2974 *
2975 * If product id not set it will create new one.
2976 *
2977 * @since 2.2.0
2978 *
2979 * @param string $title product title.
2980 * @param string $reg_price product price.
2981 * @param string $sale_price product sale price.
2982 * @param int $product_id product ID.
2983 * @param string $status product status.
2984 *
2985 * @return integer Product id or return 0 if WC not exists
2986 */
2987 public static function create_wc_product( $title, $reg_price, $sale_price, $product_id = 0, $status = 'publish' ) {
2988 if ( ! tutor_utils()->has_wc() ) {
2989 return 0;
2990 }
2991
2992 $product_obj = new \WC_Product();
2993 if ( $product_id ) {
2994 $product_obj = wc_get_product( $product_id );
2995 }
2996
2997 $product_obj->set_name( $title );
2998 $product_obj->set_status( $status );
2999 $product_obj->set_price( $reg_price );
3000 $product_obj->set_regular_price( $reg_price );
3001
3002 if ( $sale_price > 0 ) {
3003 $product_obj->set_sale_price( $sale_price );
3004 } else {
3005 $product_obj->set_sale_price( null );
3006 }
3007
3008 $product_obj->set_sold_individually( true );
3009
3010 return $product_obj->save();
3011 }
3012
3013 /**
3014 * Get course/bundle mini info
3015 *
3016 * @since 3.0.0
3017 *
3018 * @param object $post Course or bundle post.
3019 *
3020 * @return array
3021 */
3022 public static function get_mini_info( object $post ) {
3023 $is_purchasable = tutor_utils()->is_course_purchasable( $post->ID );
3024 $course_price = tutor_utils()->get_raw_course_price( $post->ID );
3025 $regular_price = tutor_get_formatted_price( $course_price->regular_price );
3026 $sale_price = ! empty( $course_price->sale_price ) ? tutor_get_formatted_price( $course_price->sale_price ) : null;
3027
3028 $info = array(
3029 'id' => $post->ID,
3030 'title' => $post->post_title,
3031 'image' => get_tutor_course_thumbnail_src( 'post-thumbnail', $post->ID ),
3032 'is_purchasable' => $is_purchasable,
3033 'regular_price' => $regular_price,
3034 'sale_price' => $sale_price,
3035 );
3036
3037 if ( 'course-bundle' === $post->post_type && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
3038 $info['total_course'] = count( BundleModel::get_bundle_course_ids( $post->ID ) );
3039 }
3040
3041 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
3042
3043 return $card_data;
3044 }
3045
3046 /**
3047 * Get course/bundle card data
3048 *
3049 * This method will return all data that contain in
3050 * course card
3051 *
3052 * @since 3.0.0
3053 *
3054 * @param object $post Course or bundle post.
3055 *
3056 * @return array
3057 */
3058 public static function get_card_data( object $post ) {
3059 $info = self::get_mini_info( $post );
3060
3061 $info['last_updated'] = tutor_i18n_get_formated_date( $post->post_modified_at );
3062 $info['course_duration'] = tutor_utils()->get_course_duration( $post->ID, false );
3063 $info['total_enrolled'] = tutor_utils()->count_enrolled_users_by_course( $post->ID );
3064
3065 $card_data = apply_filters( 'tutor_add_course_plan_info', $info, $post );
3066
3067 return $card_data;
3068 }
3069
3070 /**
3071 * Filter user list access for instructor
3072 *
3073 * @since 3.0.0
3074 *
3075 * @param bool $access access.
3076 *
3077 * @return bool
3078 */
3079 public function user_list_access_for_instructor( $access ) {
3080 $is_instructor = User::is_instructor();
3081 return $access || $is_instructor;
3082 }
3083
3084 /**
3085 * Filter user list args for instructor
3086 *
3087 * @since 3.0.0
3088 *
3089 * @param array $args args.
3090 *
3091 * @return array
3092 */
3093 public function user_list_args_for_instructor( $args ) {
3094 if ( User::is_instructor() ) {
3095 if ( isset( $args['fields'] ) && isset( $args['fields']['user_email'] ) ) {
3096 unset( $args['fields']['user_email'] );
3097 }
3098 }
3099
3100 $filter = json_decode( wp_unslash( $_POST['filter'] ?? '{}' ) );//phpcs:ignore
3101 if ( isset( $filter->role ) && is_array( $filter->role ) ) {
3102 $args['role__in'] = array_map( 'sanitize_text_field', $filter->role );
3103 }
3104
3105 return $args;
3106 }
3107 }
3108