PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 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 / Lesson.php
tutor / classes Last commit date
Addons.php 1 day ago Admin.php 1 day ago Ajax.php 1 day ago Announcements.php 1 day ago Assets.php 1 day ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 1 day ago Container.php 11 months ago Course.php 1 day ago Course_Embed.php 3 years ago Course_Filter.php 1 day ago Course_List.php 1 day ago Course_Settings_Tabs.php 1 day ago Course_Widget.php 1 year ago Custom_Validation.php 1 day ago Dashboard.php 1 day ago Earnings.php 9 months ago FormHandler.php 1 day ago Frontend.php 1 day ago Gutenberg.php 1 year ago Icon.php 1 day ago Input.php 1 day ago Instructor.php 1 day ago Instructors_List.php 1 day ago Lesson.php 1 day ago Options_V2.php 1 day ago Permalink.php 1 day ago Post_types.php 2 days ago Private_Course_Access.php 1 day ago Q_And_A.php 1 day ago Question_Answers_List.php 11 months ago Quiz.php 1 day ago QuizBuilder.php 1 day ago Quiz_Attempts_List.php 1 day ago RestAPI.php 2 years ago Reviews.php 1 day ago Rewrite_Rules.php 2 years ago SampleCourse.php 1 day ago Shortcode.php 1 day ago Singleton.php 1 year ago Student.php 1 day ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 1 day ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 4 weeks ago Tutor.php 1 day ago TutorEDD.php 1 day ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 day ago Upgrader.php 1 day ago User.php 1 day ago UserPreference.php 1 day ago Utils.php 1 day ago Video_Stream.php 3 years ago WhatsNew.php 10 months ago Withdraw.php 1 day ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 1 day ago
Lesson.php
1250 lines
1 <?php
2 /**
3 * Manage Lesson
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 defined( 'ABSPATH' ) || exit;
14
15 use Tutor\Helpers\HttpHelper;
16 use Tutor\Helpers\QueryHelper;
17 use Tutor\Helpers\ValidationHelper;
18 use Tutor\Models\EnrollmentModel;
19 use Tutor\Models\LessonModel;
20 use Tutor\Traits\JsonResponse;
21 use WP_Post;
22
23 /**
24 * Lesson class
25 *
26 * @since 1.0.0
27 */
28 class Lesson extends Tutor_Base {
29 use JsonResponse;
30
31 /**
32 * Preview meta key
33 *
34 * @since 4.0.0
35 * @var string
36 */
37 const PREVIEW_META_KEY = '_is_preview';
38
39 /**
40 * Lesson post type
41 *
42 * @since 4.0.0
43 *
44 * @var string
45 */
46 private $post_type;
47
48 /**
49 * Determine if legacy mode is enabled or not
50 *
51 * @since 4.0.0
52 *
53 * @var bool
54 */
55 private $is_legacy_learning_mode = false;
56
57 /**
58 * Register hooks
59 *
60 * @since 1.0.0
61 * @since 3.7.0 param register hook added.
62 *
63 * @param bool $register_hooks register hooks or not.
64 *
65 * @return void
66 */
67 public function __construct( $register_hooks = true ) {
68 parent::__construct();
69
70 $this->post_type = tutor()->lesson_post_type;
71
72 $this->is_legacy_learning_mode = Options_V2::LEARNING_MODE_LEGACY === tutor_utils()->get_option( 'learning_mode' );
73
74 if ( ! $register_hooks ) {
75 return;
76 }
77
78 add_action( 'save_post_' . $this->lesson_post_type, array( $this, 'save_lesson_meta' ) );
79
80 add_action( 'wp_ajax_tutor_lesson_details', array( $this, 'ajax_lesson_details' ) );
81 add_action( 'wp_ajax_tutor_save_lesson', array( $this, 'ajax_save_lesson' ) );
82 add_action( 'wp_ajax_tutor_delete_lesson', array( $this, 'ajax_delete_lesson' ) );
83
84 add_filter( 'get_sample_permalink', array( $this, 'change_lesson_permalink' ), 10, 2 );
85
86 /**
87 * Frontend Action
88 */
89 add_action( 'template_redirect', array( $this, 'mark_lesson_complete' ) );
90
91 add_action( 'wp_ajax_tutor_render_lesson_content', array( $this, 'tutor_render_lesson_content' ) );
92
93 /**
94 * For public course access
95 */
96 add_action( 'wp_ajax_nopriv_tutor_render_lesson_content', array( $this, 'tutor_render_lesson_content' ) );
97
98 /**
99 * Autoplay next video
100 *
101 * @since 1.4.9
102 */
103 add_action( 'wp_ajax_autoload_next_course_content', array( $this, 'autoload_next_course_content' ) );
104
105 /**
106 * Load next course item after click complete button
107 *
108 * @since 1.5.3
109 */
110 add_action( 'tutor_lesson_completed_after', array( $this, 'tutor_lesson_completed_after' ), 999 );
111
112 /**
113 * Lesson comment & reply ajax handler
114 *
115 * @since 2.0.0
116 */
117 add_action( 'wp_ajax_tutor_single_course_lesson_load_more', array( $this, 'ajax_single_course_lesson_load_more' ) );
118 add_action( 'wp_ajax_tutor_create_lesson_comment', array( $this, 'ajax_single_course_lesson_load_more' ) );
119 add_action( 'wp_ajax_tutor_delete_lesson_comment', array( $this, 'ajax_delete_lesson_comment' ) );
120 add_action( 'wp_ajax_tutor_update_lesson_comment', array( $this, 'ajax_update_lesson_comment' ) );
121 add_action( 'wp_ajax_tutor_reply_lesson_comment', array( $this, 'ajax_reply_lesson_comment' ) );
122 add_action( 'wp_ajax_tutor_load_lesson_comments', array( $this, 'ajax_load_lesson_comments' ) );
123 add_action( 'wp_ajax_tutor_load_comment_replies', array( $this, 'ajax_load_comment_replies' ) );
124
125 // Add lesson title as nav item & render single content on the learning area.
126 add_action( "tutor_learning_area_nav_item_{$this->post_type}", array( $this, 'render_nav_item' ), 10, 2 );
127 add_action( "tutor_single_content_{$this->post_type}", array( $this, 'render_single_content' ) );
128 }
129
130 /**
131 * Manage load more & comment create
132 *
133 * @since 2.0.6
134 *
135 * @since 4.0.0 different template returned
136 *
137 * @return void send wp json data
138 */
139 public function ajax_single_course_lesson_load_more() {
140 tutor_utils()->checking_nonce();
141
142 $comment_id = 0;
143 $comment = Input::post( 'comment', '', Input::TYPE_TEXTAREA );
144 $lesson_id = Input::post( 'comment_post_ID', 0, Input::TYPE_INT );
145
146 if ( ! self::is_comment_enabled_for_lesson( $lesson_id ) ) {
147 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
148 }
149
150 if ( 'tutor_create_lesson_comment' === Input::post( 'action' ) && strlen( $comment ) > 0 ) {
151 if ( ! self::can_post_lesson_comment( $lesson_id ) ) {
152 wp_send_json_error( tutor_utils()->error_message() );
153 return;
154 }
155
156 $comment_data = array(
157 'comment_content' => $comment,
158 'comment_post_ID' => $lesson_id,
159 'comment_parent' => Input::post( 'comment_parent', 0, Input::TYPE_INT ),
160 );
161 $comment_id = self::create_comment( $comment_data );
162 do_action( 'tutor_new_comment_added', $comment_data );
163 }
164
165 if ( ! $this->is_legacy_learning_mode ) {
166 $html = '';
167 if ( $comment_id ) {
168 ob_start();
169 tutor_load_template(
170 'learning-area.lesson.comment-card',
171 array(
172 'comment_item' => get_comment( $comment_id ),
173 'lesson_id' => $lesson_id,
174 'user_id' => get_current_user_id(),
175 )
176 );
177 $html = ob_get_clean();
178 }
179
180 $count = self::get_comments(
181 array(
182 'post_id' => $lesson_id,
183 'parent' => 0,
184 'count' => true,
185 )
186 );
187
188 wp_send_json_success(
189 array(
190 'html' => $html,
191 'count' => $count,
192 )
193 );
194 }
195
196 ob_start();
197 tutor_load_template( 'single.lesson.comment' );
198 $html = ob_get_clean();
199 wp_send_json_success( array( 'html' => $html ) );
200 }
201
202 /**
203 * Delete lesson comment by AJAX
204 *
205 * @since 4.0.0
206 *
207 * @return void
208 */
209 public function ajax_delete_lesson_comment() {
210 tutor_utils()->check_nonce();
211 $comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT );
212 if ( ! $comment_id ) {
213 $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
214 }
215
216 $comment = get_comment( $comment_id );
217 if ( ! $comment ) {
218 $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
219 }
220
221 $lesson_id = $comment->comment_post_ID;
222 $parent_id = $comment->comment_parent;
223 $is_reply = $parent_id > 0;
224
225 if ( ! self::is_comment_enabled_for_lesson( $lesson_id ) ) {
226 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
227 }
228
229 if ( get_current_user_id() === (int) $comment->user_id || tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) {
230 wp_delete_comment( $comment_id, true );
231
232 $total_comments = self::get_comments(
233 array(
234 'post_id' => $lesson_id,
235 'parent' => 0,
236 'count' => true,
237 )
238 );
239
240 wp_send_json_success(
241 array(
242 'message' => __( 'Comment deleted successfully', 'tutor' ),
243 'count' => $total_comments,
244 'is_reply' => $is_reply,
245 'parent_id' => $parent_id,
246 'comment_id' => $comment_id,
247 )
248 );
249 } else {
250 $this->response_bad_request( __( 'You are not allowed to delete this comment', 'tutor' ) );
251 }
252 }
253
254 /**
255 * Update lesson comment by AJAX
256 *
257 * @since 4.0.0
258 *
259 * @return void
260 */
261 public function ajax_update_lesson_comment() {
262 tutor_utils()->check_nonce();
263
264 $comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT );
265 if ( ! $comment_id ) {
266 $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
267 }
268
269 $comment = get_comment( $comment_id );
270 if ( ! $comment ) {
271 $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
272 }
273
274 $comment_content = Input::post( 'comment', '', Input::TYPE_KSES_POST );
275 $user_id = get_current_user_id();
276 $lesson_id = $comment->comment_post_ID;
277
278 if ( ! self::is_comment_enabled_for_lesson( $lesson_id ) ) {
279 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
280 }
281
282 if ( $user_id === (int) $comment->user_id ) {
283 wp_update_comment(
284 array(
285 'comment_ID' => $comment_id,
286 'comment_content' => $comment_content,
287 )
288 );
289
290 $comment->comment_content = $comment_content;
291 $is_reply = $comment->comment_parent > 0;
292
293 ob_start();
294 tutor_load_template(
295 'learning-area.lesson.comment-card',
296 array(
297 'comment_item' => $comment,
298 'lesson_id' => $lesson_id,
299 'user_id' => $user_id,
300 'is_reply' => $is_reply,
301 )
302 );
303 $html = ob_get_clean();
304
305 wp_send_json_success(
306 array(
307 'html' => $html,
308 'is_reply' => $is_reply,
309 'parent_id' => $is_reply ? $comment->comment_parent : 0,
310 'comment_id' => $comment_id,
311 )
312 );
313 } else {
314 $this->response_bad_request( tutor_utils()->error_message() );
315 }
316 }
317
318 /**
319 * Saving lesson meta and assets
320 *
321 * @since 1.0.0
322 *
323 * @param integer $post_ID post ID.
324 *
325 * @return void
326 */
327 public function save_lesson_meta( $post_ID ) {
328 $thumbnail_id = Input::post( 'thumbnail_id', 0, Input::TYPE_INT );
329 if ( $thumbnail_id ) {
330 update_post_meta( $post_ID, '_thumbnail_id', $thumbnail_id );
331 } else {
332 delete_post_meta( $post_ID, '_thumbnail_id' );
333 }
334
335 $video_source = sanitize_text_field( tutor_utils()->array_get( 'video.source', $_POST ) ); //phpcs:ignore
336 if ( '-1' === $video_source ) {
337 delete_post_meta( $post_ID, '_video' );
338 } elseif ( $video_source ) {
339
340 // Sanitize data through helper method.
341 $video = Input::sanitize_array(
342 $_POST['video'] ?? array(), //phpcs:ignore
343 array(
344 'source_external_url' => 'esc_url',
345 'source_embedded' => 'wp_kses_post',
346 ),
347 true
348 );
349 update_post_meta( $post_ID, '_video', $video );
350 }
351
352 // Attachments.
353 $attachments = array();
354 // phpcs:disable WordPress.Security.NonceVerification.Missing
355 if ( ! empty( $_POST['tutor_attachments'] ) ) {
356 //phpcs:ignore -- data sanitized through helper method.
357 $attachments = tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_attachments'] ) );
358 $attachments = array_unique( $attachments );
359 }
360
361 /**
362 * If !empty attachment then update meta else
363 * delete meta key to prevent empty data in db
364 *
365 * @since 1.8.9
366 */
367 if ( ! empty( $attachments ) ) {
368 update_post_meta( $post_ID, '_tutor_attachments', $attachments );
369 } else {
370 delete_post_meta( $post_ID, '_tutor_attachments' );
371 }
372 }
373
374 /**
375 * Get lesson details data.
376 *
377 * @since 3.0.0
378 *
379 * @return void
380 */
381 public function ajax_lesson_details() {
382 if ( ! tutor_utils()->is_nonce_verified() ) {
383 $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
384 }
385
386 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
387 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
388
389 if ( ! $topic_id || ! $lesson_id ) {
390 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
391 }
392
393 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) || ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) {
394 $this->response_bad_request( tutor_utils()->error_message() );
395 }
396
397 $data = LessonModel::get_lesson_details( $lesson_id );
398
399 $this->json_response(
400 __( 'Lesson data fetched successfully', 'tutor' ),
401 $data
402 );
403 }
404
405 /**
406 * Create or update lesson.
407 *
408 * @since 1.0.0
409 * @since 1.5.1 updated
410 * @since 3.0.0 refactor and response updated.
411 *
412 * @return void
413 */
414 public function ajax_save_lesson() {
415 if ( ! tutor_utils()->is_nonce_verified() ) {
416 $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
417 }
418
419 /**
420 * Allow iframe inside lesson content to support
421 * embed video & other stuff
422 *
423 * @since 2.1.6
424 */
425 add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
426
427 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
428 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
429
430 if ( ! $topic_id || ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
431 $this->response_bad_request( tutor_utils()->error_message() );
432 }
433
434 $is_update = $lesson_id > 0;
435
436 $title = Input::post( 'title' );
437 $description = Input::post( 'description', '', Input::TYPE_KSES_POST );
438 $is_html_active = Input::post( 'is_html_active' ) === 'true' ? true : false;
439 $raw_html_content = Input::post( 'tutor_lesson_modal_editor', '', Input::TYPE_KSES_POST );
440 $post_content = $is_html_active ? $raw_html_content : $description;
441
442 $rules = array(
443 'topic_id' => 'required|numeric',
444 'lesson_id' => 'if_input|numeric',
445 );
446
447 //phpcs:ignore WordPress.Security.NonceVerification.Missing
448 $params = Input::sanitize_array( $_POST, array( 'description' => 'wp_kses_post' ) );
449
450 $validation = ValidationHelper::validate( $rules, $params );
451 if ( ! $validation->success ) {
452 $this->json_response(
453 __( 'Invalid inputs', 'tutor' ),
454 $validation->errors,
455 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
456 );
457 }
458
459 $lesson_data = array(
460 'post_type' => $this->lesson_post_type,
461 'post_title' => wp_slash( $title ), // Note: Added wp_slash to support latex syntaxes.
462 'post_name' => sanitize_title( $title ),
463 'post_content' => wp_slash( $post_content ),
464 'post_status' => 'publish',
465 'comment_status' => 'open',
466 'post_author' => get_current_user_id(),
467 'post_parent' => $topic_id,
468 );
469
470 if ( ! $is_update ) {
471 $lesson_data['menu_order'] = tutor_utils()->get_next_course_content_order_id( $topic_id );
472 $lesson_id = wp_insert_post( $lesson_data );
473
474 if ( $lesson_id ) {
475 do_action( 'tutor/lesson/created', $lesson_id );
476 } else {
477 $this->json_response(
478 tutor_utils()->error_message(),
479 null,
480 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
481 );
482 }
483 } else {
484 $lesson_data['ID'] = $lesson_id;
485
486 if ( ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) {
487 $this->json_response(
488 tutor_utils()->error_message(),
489 null,
490 HttpHelper::STATUS_FORBIDDEN
491 );
492 }
493
494 do_action( 'tutor/lesson_update/before', $lesson_id );
495 wp_update_post( $lesson_data );
496 do_action( 'tutor/lesson_update/after', $lesson_id );
497 }
498
499 if ( $is_update ) {
500 $this->json_response(
501 __( 'Lesson updated successfully', 'tutor' ),
502 $lesson_id
503 );
504 } else {
505 $this->json_response(
506 __( 'Lesson created successfully', 'tutor' ),
507 $lesson_id,
508 HttpHelper::STATUS_CREATED
509 );
510 }
511 }
512
513 /**
514 * Delete Lesson from course builder by ID
515 *
516 * @since 1.0.0
517 * @since 3.0.0 refactor and update response.
518 *
519 * @return void
520 */
521 public function ajax_delete_lesson() {
522 tutor_utils()->check_nonce();
523
524 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
525 if ( ! $lesson_id ) {
526 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
527 }
528
529 if ( ! tutor_utils()->can_user_manage( 'lesson', $lesson_id ) ) {
530 $this->response_bad_request( tutor_utils()->error_message() );
531 }
532
533 $content = __( 'Lesson', 'tutor' );
534 $post_type = get_post_type( $lesson_id );
535 if ( tutor()->assignment_post_type === $post_type ) {
536 $content = __( 'Assignment', 'tutor' );
537 }
538
539 do_action( 'tutor_before_delete_course_content', 0, $lesson_id );
540
541 wp_delete_post( $lesson_id, true );
542 /* translators: %s refers to the name of the content being deleted */
543 $this->json_response( sprintf( __( '%s deleted successfully', 'tutor' ), $content ) );
544 }
545
546
547 /**
548 * Changed the URI based
549 *
550 * @since 1.0.0
551 *
552 * @param string $uri URI.
553 * @param integer $lesson_id lesson ID.
554 *
555 * @return string
556 */
557 public function change_lesson_permalink( $uri, $lesson_id ) {
558 $post = get_post( $lesson_id );
559
560 if ( $post && $post->post_type === $this->lesson_post_type ) {
561 $uri_base = trailingslashit( site_url() );
562
563 $sample_course = 'sample-course';
564 $is_course = tutor_utils()->get_course_id_by( 'lesson', get_the_ID() );
565 if ( $is_course ) {
566 $course = get_post( $is_course );
567 if ( $course ) {
568 $sample_course = $course->post_name;
569 }
570 }
571
572 $new_course_base = $uri_base . "course/{$sample_course}/lesson/%pagename%/";
573 $uri[0] = $new_course_base;
574 }
575
576 return $uri;
577 }
578
579 /**
580 * Mark lesson completed
581 *
582 * @since 1.0.0
583 *
584 * @return void
585 */
586 public function mark_lesson_complete() {
587 if ( 'tutor_complete_lesson' !== Input::post( 'tutor_action' ) ) {
588 return;
589 }
590
591 tutor_utils()->checking_nonce();
592
593 $user_id = get_current_user_id();
594
595 if ( ! $user_id ) {
596 die( esc_html__( 'Please Sign-In', 'tutor' ) );
597 }
598
599 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
600 if ( ! $lesson_id ) {
601 return;
602 }
603
604 $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id );
605 if ( ! $course_id ) {
606 return;
607 }
608
609 if ( ! tutor_utils()->is_enrolled( $course_id ) ) {
610 die( esc_html( tutor_utils()->error_message() ) );
611 }
612
613 $validated = apply_filters( 'tutor_validate_lesson_complete', true, $user_id, $lesson_id );
614 if ( ! $validated ) {
615 return;
616 }
617
618 do_action( 'tutor_lesson_completed_before', $lesson_id );
619 /**
620 * Marking lesson at user meta, meta format, _tutor_completed_lesson_id_{id} and value = tutor_time();
621 */
622 LessonModel::mark_lesson_complete( $lesson_id );
623
624 do_action( 'tutor_lesson_completed_email_after', $lesson_id, $user_id );
625 do_action( 'tutor_lesson_completed_after', $lesson_id, $user_id );
626 }
627
628 /**
629 * Render the lesson content
630 *
631 * @since 1.0.0
632 *
633 * @return void
634 */
635 public function tutor_render_lesson_content() {
636 tutor_utils()->checking_nonce();
637
638 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
639 if ( ! $lesson_id ) {
640 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
641 }
642
643 $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id );
644
645 // Course must be public or current user must be enrolled to access this lesson.
646 if ( ! Course_List::is_public( $course_id ) && ! EnrollmentModel::is_enrolled( $course_id ) ) {
647
648 $allowed = User::is_admin() ? true : tutor_utils()->is_instructor_of_this_course( get_current_user_id(), $course_id );
649
650 if ( ! $allowed ) {
651 $this->response_bad_request( tutor_utils()->error_message() );
652 exit;
653 }
654 }
655
656 ob_start();
657 global $post;
658
659 $post = get_post( $lesson_id ); //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
660 setup_postdata( $post );
661 tutor_lesson_content();
662 wp_reset_postdata();
663
664 $html = ob_get_clean();
665
666 wp_send_json_success( array( 'html' => $html ) );
667 }
668
669 /**
670 * Load next course item automatically
671 *
672 * @since 1.4.9
673 *
674 * @return void
675 */
676 public function autoload_next_course_content() {
677 tutor_utils()->checking_nonce();
678
679 $post_id = Input::post( 'post_id', 0, Input::TYPE_INT );
680 $course_id = tutor_utils()->get_course_id_by_content( $post_id );
681 if ( ! $course_id || ! EnrollmentModel::is_enrolled( $course_id ) ) {
682 wp_send_json_error( tutor_utils()->error_message() );
683 }
684
685 $content_id = tutor_utils()->get_post_id( $post_id );
686 $contents = tutor_utils()->get_course_prev_next_contents_by_id( $content_id );
687
688 $autoload_course_content = (bool) UserPreference::get( 'auto_play_next', false, get_current_user_id() );
689 $next_url = false;
690 if ( $autoload_course_content && $contents->next_id ) {
691 $next_url = get_the_permalink( $contents->next_id );
692 }
693
694 wp_send_json_success( array( 'next_url' => $next_url ) );
695 }
696
697 /**
698 * Load next course item after click complete button
699 *
700 * @since 1.5.3
701 *
702 * @param integer $content_id content ID.
703 * @return void
704 */
705 public function tutor_lesson_completed_after( $content_id ) {
706 $contents = tutor_utils()->get_course_prev_next_contents_by_id( $content_id );
707 $autoload_course_content = (bool) get_tutor_option( 'autoload_next_course_content' );
708 if ( $autoload_course_content ) {
709 wp_safe_redirect( get_the_permalink( $contents->next_id ) );
710 } else {
711 wp_safe_redirect( get_the_permalink( $content_id ) );
712 }
713 die();
714 }
715
716 /**
717 * Check user can post lesson comment.
718 *
719 * @since 4.0.0
720 *
721 * @param int $lesson_id lesson id.
722 * @param int $user_id user id. default 0 means current user id.
723 *
724 * @return bool true if user can post lesson comment, false otherwise.
725 */
726 private static function can_post_lesson_comment( $lesson_id, $user_id = 0 ) {
727 $course_id = tutor_utils()->get_course_id_by( 'lesson', $lesson_id );
728 if ( ! $course_id ) {
729 return false;
730 }
731
732 $can_post = EnrollmentModel::is_enrolled( $course_id, $user_id ) || tutor_utils()->can_user_manage( 'course', $course_id, $user_id );
733 return $can_post;
734 }
735
736 /**
737 * Replay lesson comment
738 *
739 * @since 1.0.0
740 *
741 * @return void|null
742 */
743 public function ajax_reply_lesson_comment() {
744 tutor_utils()->checking_nonce();
745
746 $lesson_id = Input::post( 'comment_post_ID', 0, Input::TYPE_INT );
747 $comment_parent = Input::post( 'comment_parent', 0, Input::TYPE_INT );
748 $comment = Input::post( 'comment', '', Input::TYPE_TEXTAREA );
749
750 if ( ! $lesson_id || ! $comment_parent || 0 === strlen( $comment ) ) {
751 wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
752 return;
753 }
754
755 if ( ! self::can_post_lesson_comment( $lesson_id ) ) {
756 wp_send_json_error( tutor_utils()->error_message() );
757 return;
758 }
759
760 $parent_comment = get_comment( $comment_parent );
761 if ( ! $parent_comment || (int) $parent_comment->comment_post_ID !== $lesson_id ) {
762 wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
763 return;
764 }
765
766 $comment_data = array(
767 'comment_content' => $comment,
768 'comment_post_ID' => $lesson_id,
769 'comment_parent' => $comment_parent,
770 );
771
772 if ( ! self::is_comment_enabled_for_lesson( $comment_data['comment_post_ID'] ) ) {
773 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
774 }
775
776 $comment_id = self::create_comment( $comment_data );
777 if ( false === $comment_id ) {
778 wp_send_json_error();
779 return;
780 }
781
782 $reply = get_comment( $comment_id );
783 do_action( 'tutor_reply_lesson_comment_thread', $comment_id, $comment_data );
784
785 if ( ! $this->is_legacy_learning_mode ) {
786 $lesson_id = $comment_data['comment_post_ID'];
787 $parent_id = $comment_data['comment_parent'];
788 $comment_item = get_comment( $parent_id );
789 $user_id = get_current_user_id();
790
791 $replies = self::get_comments(
792 array(
793 'post_id' => $lesson_id,
794 'parent' => $parent_id,
795 'order' => 'ASC',
796 )
797 );
798
799 $is_first_reply = ( is_array( $replies ) ? count( $replies ) : 0 ) === 1;
800
801 ob_start();
802 if ( $is_first_reply ) {
803 // For the first reply, we need the wrapper and the toggle button.
804 tutor_load_template(
805 'learning-area.lesson.comment-replies',
806 array(
807 'lesson_id' => $lesson_id,
808 'comment_item' => $comment_item,
809 'user_id' => $user_id,
810 'replies' => $replies,
811 )
812 );
813 } else {
814 // Just the card for subsequent replies.
815 tutor_load_template(
816 'learning-area.lesson.comment-card',
817 array(
818 'comment_item' => get_comment( $comment_id ),
819 'lesson_id' => $lesson_id,
820 'user_id' => $user_id,
821 'is_reply' => true,
822 )
823 );
824 }
825 $html = ob_get_clean();
826
827 $total_comments = self::get_comments(
828 array(
829 'post_id' => $lesson_id,
830 'parent' => 0,
831 'count' => true,
832 )
833 );
834
835 wp_send_json_success(
836 array(
837 'html' => $html,
838 'count' => $total_comments,
839 'is_first_reply' => $is_first_reply,
840 )
841 );
842 }
843
844 ob_start();
845 ?>
846 <div class="tutor-comments-list tutor-child-comment tutor-mt-32" id="lesson-comment-<?php echo esc_attr( $reply->comment_ID ); ?>">
847 <div class="comment-avatar">
848 <img src="<?php echo esc_url( get_avatar_url( $reply->user_id ) ); ?>" alt="">
849 </div>
850 <div class="tutor-single-comment">
851 <div class="tutor-actual-comment tutor-mb-12">
852 <div class="tutor-comment-author">
853 <span class="tutor-fs-6 tutor-fw-bold">
854 <?php echo esc_html( $reply->comment_author ); ?>
855 </span>
856 <span class="tutor-fs-7 tutor-ml-0 tutor-ml-sm-10">
857 <?php echo esc_html( human_time_diff( strtotime( $reply->comment_date ), tutor_time() ) . __( ' ago', 'tutor' ) ); ?>
858 </span>
859 </div>
860 <div class="tutor-comment-text tutor-fs-6 tutor-mt-4">
861 <?php echo esc_html( $reply->comment_content ); ?>
862 </div>
863 </div>
864 </div>
865 </div>
866 <?php
867 $html = ob_get_clean();
868 wp_send_json_success( array( 'html' => $html ) );
869 }
870
871 /**
872 * Get comments
873 *
874 * @since 2.0.6
875 * @see Checkout arguments details: https://developer.wordpress.org/reference/classes/wp_comment_query/__construct/
876 *
877 * @param array $args arguments.
878 * @return mixed based on arguments
879 */
880 public static function get_comments( array $args ) {
881 $args['type'] = 'comment';
882 $comments = get_comments( $args );
883 return $comments;
884 }
885
886 /**
887 * Get comment replies
888 *
889 * @since 4.0.0
890 *
891 * @param int $comment_id comment id.
892 * @param string $order order.
893 *
894 * @return mixed comment replies on success, false on failure
895 */
896 public static function get_comment_replies( int $comment_id, string $order = 'DESC' ) {
897 return get_comments(
898 array(
899 'parent' => $comment_id,
900 'order' => QueryHelper::get_valid_sort_order( $order ),
901 )
902 );
903 }
904
905 /**
906 * Create comment
907 *
908 * @since 1.0.0
909 *
910 * @param array $request request.
911 * @return mixed comment id on success, false on failure
912 */
913 public static function create_comment( array $request ) {
914 $current_user = wp_get_current_user();
915 $default_data = array(
916 'comment_content' => '',
917 'comment_post_ID' => '',
918 'comment_parent' => '',
919 'user_id' => $current_user->ID,
920 'comment_author' => $current_user->user_login,
921 'comment_author_email' => $current_user->user_email,
922 'comment_author_url' => $current_user->user_url,
923 'comment_agent' => 'Tutor',
924 );
925 $comment_data = wp_parse_args( $request, $default_data );
926 return wp_insert_comment( $comment_data );
927 }
928
929 /**
930 * Check if lesson has content
931 *
932 * @since 3.9.0
933 *
934 * @param int $lesson_id Lesson ID.
935 *
936 * @return bool True if lesson has content, false otherwise.
937 */
938 public static function has_lesson_content( $lesson_id ) {
939 /**
940 * If lesson has no content, lesson tab will be hidden.
941 * To enable elementor and SCORM, only admin can see lesson tab.
942 *
943 * @since 2.2.2
944 */
945 return apply_filters(
946 'tutor_has_lesson_content',
947 User::is_admin() || ! in_array( trim( get_the_content() ), array( null, '', '&nbsp;' ), true ),
948 $lesson_id
949 );
950 }
951
952 /**
953 * Check if lesson has attachments
954 *
955 * @since 3.9.0
956 *
957 * @param int $lesson_id Lesson ID.
958 *
959 * @return bool True if lesson has attachments, false otherwise.
960 */
961 public static function has_lesson_attachment( $lesson_id ) {
962 return count( tutor_utils()->get_attachments( $lesson_id ) ) > 0;
963 }
964
965 /**
966 * Check if comments are enabled for lessons
967 *
968 * @since 3.9.0
969 *
970 * @return bool True if comments are enabled, false otherwise.
971 */
972 public static function is_comment_enabled() {
973 return tutor_utils()->get_option( 'enable_comment_for_lesson' );
974 }
975
976 /**
977 * Check if comments are enabled for lessons
978 *
979 * @since 4.0.0
980 *
981 * @param int|object $lesson Lesson ID or object. If null, current lesson will be used.
982 *
983 * @return bool True if comments are enabled, false otherwise.
984 */
985 public static function is_comment_enabled_for_lesson( $lesson = null ) {
986 return self::is_comment_enabled() && comments_open( $lesson );
987 }
988
989 /**
990 * Check if lesson has comments
991 *
992 * @since 3.9.0
993 *
994 * @param int $lesson_id Lesson ID.
995 *
996 * @return int Number of comments for the lesson.
997 */
998 public static function has_lesson_comment( $lesson_id ) {
999 return (int) get_comments_number( $lesson_id );
1000 }
1001
1002 /**
1003 * Get navigation items for lesson single page
1004 *
1005 * @since 3.9.0
1006 *
1007 * @param int $lesson_id Lesson ID.
1008 *
1009 * @return array navigation items array
1010 */
1011 public static function get_nav_items( $lesson_id ) {
1012 $is_legacy = ( new self( false ) )->is_legacy_learning_mode;
1013 $nav_items = array();
1014
1015 $attachments = tutor_utils()->get_attachments( $lesson_id );
1016
1017 $definitions = array(
1018 'overview' => array(
1019 'condition' => self::has_lesson_content( $lesson_id ),
1020 'legacy' => array(
1021 'label' => __( 'Overview', 'tutor' ),
1022 'value' => 'overview',
1023 'icon' => 'document-text',
1024 'template' => 'single.lesson.parts.overview',
1025 ),
1026 'default' => array(
1027 'id' => 'overview',
1028 'label' => __( 'Overview', 'tutor' ),
1029 'icon' => Icon::COURSES,
1030 'template' => 'learning-area.lesson.overview',
1031 ),
1032 ),
1033 'files' => array(
1034 'condition' => tutor_utils()->count( $attachments ),
1035 'legacy' => array(
1036 'label' => __( 'Exercise Files', 'tutor' ),
1037 'value' => 'files',
1038 'icon' => 'paperclip',
1039 'template' => 'single.lesson.parts.files',
1040 ),
1041 'default' => array(
1042 'id' => 'exercise_files',
1043 'label' => __( 'Exercise Files', 'tutor' ),
1044 'icon' => Icon::FILE_ATTACHEMENT,
1045 'template' => 'learning-area.lesson.exercise-files',
1046 ),
1047 ),
1048 'comments' => array(
1049 'condition' => self::is_comment_enabled_for_lesson( $lesson_id ) && is_user_logged_in(),
1050 'legacy' => array(
1051 'label' => __( 'Comments', 'tutor' ),
1052 'value' => 'comments',
1053 'icon' => 'comment',
1054 'template' => 'single.lesson.parts.comments',
1055 ),
1056 'default' => array(
1057 'id' => 'comments',
1058 'label' => __( 'Comments', 'tutor' ),
1059 'icon' => Icon::COMMENTS,
1060 'template' => 'learning-area.lesson.comments',
1061 ),
1062 ),
1063 );
1064
1065 foreach ( $definitions as $key => $def ) {
1066 if ( empty( $def['condition'] ) ) {
1067 continue;
1068 }
1069 $item = $is_legacy ? $def['legacy'] : $def['default'];
1070 if ( null !== $item ) {
1071 $nav_items[ $key ] = $item;
1072 }
1073 }
1074
1075 $nav_items = apply_filters( 'tutor_lesson_single_nav_items', $nav_items );
1076 $nav_items = array_values( $nav_items );
1077
1078 return $nav_items;
1079 }
1080
1081 /**
1082 * Return lesson title as nav item to print on the learning area
1083 *
1084 * @since 4.0.0
1085 *
1086 * @param WP_Post $lesson Lesson post object.
1087 * @param bool $can_access Can user access this content.
1088 *
1089 * @return void
1090 */
1091 public function render_nav_item( $lesson, $can_access ): void {
1092 tutor_load_template(
1093 'learning-area.lesson.nav-item',
1094 array(
1095 'lesson' => $lesson,
1096 'can_access' => $can_access,
1097 )
1098 );
1099 }
1100
1101 /**
1102 * Render content for the a single lesson
1103 *
1104 * @since 4.0.0
1105 *
1106 * @param WP_Post $lesson Lesson post object.
1107 *
1108 * @return void
1109 */
1110 public function render_single_content( $lesson ): void {
1111 tutor_load_template(
1112 'learning-area.lesson.content',
1113 array(
1114 'lesson' => $lesson,
1115 )
1116 );
1117 }
1118
1119 /**
1120 * Load lesson comments
1121 *
1122 * @since 4.0.0
1123 *
1124 * @return void
1125 */
1126 public function ajax_load_lesson_comments() {
1127 tutor_utils()->check_nonce();
1128
1129 $lesson_id = Input::post( 'lesson_id', 0, Input::TYPE_INT );
1130 $current_page = Input::post( 'current_page', 1, Input::TYPE_INT );
1131 $offset = Input::post( 'offset', -1, Input::TYPE_INT );
1132 $order = QueryHelper::get_valid_sort_order( Input::post( 'order', 'DESC' ) );
1133
1134 if ( ! self::is_comment_enabled_for_lesson( $lesson_id ) ) {
1135 $this->response_bad_request( tutor_utils()->error_message( 'invalid_req' ) );
1136 }
1137
1138 $user_id = get_current_user_id();
1139 $item_per_page = tutor_utils()->get_option( 'pagination_per_page', 10 );
1140
1141 $query_args = array(
1142 'post_id' => $lesson_id,
1143 'parent' => 0,
1144 'number' => $item_per_page,
1145 'order' => $order,
1146 );
1147
1148 if ( $offset >= 0 ) {
1149 $query_args['offset'] = $offset;
1150 } else {
1151 $query_args['paged'] = $current_page;
1152 }
1153
1154 $comment_list = self::get_comments( $query_args );
1155
1156 // Get total comment count to determine if there are more pages.
1157 $total_comments = self::get_comments(
1158 array(
1159 'post_id' => $lesson_id,
1160 'parent' => 0,
1161 'count' => true,
1162 )
1163 );
1164
1165 ob_start();
1166 tutor_load_template(
1167 'learning-area.lesson.comment-list',
1168 compact( 'comment_list', 'lesson_id', 'user_id' )
1169 );
1170 $html = ob_get_clean();
1171
1172 // Calculate if there are more items.
1173 if ( $offset >= 0 ) {
1174 $items_loaded = $offset + count( $comment_list );
1175 } else {
1176 $items_loaded = $current_page * $item_per_page;
1177 }
1178 $has_more = $items_loaded < $total_comments;
1179
1180 wp_send_json_success(
1181 array(
1182 'html' => $html,
1183 'has_more' => $has_more,
1184 'count' => $total_comments,
1185 )
1186 );
1187 }
1188
1189 /**
1190 * Load comment replies for dashboard.
1191 *
1192 * @since 4.0.0
1193 *
1194 * @return void
1195 */
1196 public function ajax_load_comment_replies() {
1197 tutor_utils()->check_nonce();
1198
1199 $comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT );
1200 $replies_order = Input::post( 'order', 'DESC' );
1201
1202 if ( ! $comment_id ) {
1203 $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
1204 }
1205
1206 $user_id = get_current_user_id();
1207 $replies = self::get_comment_replies( $comment_id, $replies_order );
1208
1209 ob_start();
1210 tutor_load_template(
1211 'dashboard.discussions.comment-replies',
1212 array(
1213 'replies' => $replies,
1214 'replies_order' => $replies_order,
1215 'user_id' => $user_id,
1216 )
1217 );
1218 $html = ob_get_clean();
1219
1220 wp_send_json_success( array( 'html' => $html ) );
1221 }
1222
1223 /**
1224 * Get lesson content type info with video duration
1225 *
1226 * @since 4.0.0
1227 *
1228 * @param WP_Post $lesson Lesson post.
1229 *
1230 * @return string
1231 */
1232 public static function get_content_type_info( WP_Post $lesson ) {
1233 $video_info = tutor_utils()->get_video_info( $lesson->ID );
1234 $lesson_type = __( 'Reading', 'tutor' );
1235
1236 if ( $video_info ) {
1237 $lesson_type = __( 'Video', 'tutor' );
1238 $playtime = $video_info->playtime ?? '';
1239
1240 // Check if the playtime is actually a valid, positive duration.
1241 if ( ! empty( $playtime ) ) {
1242 /* translators: %s: duration in minutes */
1243 $lesson_type = sprintf( __( 'Video - %s mins', 'tutor' ), $playtime );
1244 }
1245 }
1246
1247 return $lesson_type;
1248 }
1249 }
1250