PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 All 140 releases
learnpress / inc / jwt / rest-api / version1 / class-lp-rest-lessons-v1-controller.php

class-lp-rest-lessons-v1-controller.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.9, at inc/jwt/rest-api/version1/class-lp-rest-lessons-v1-controller.php

700 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use LearnPress\Models\CourseModel;
4 use LearnPress\Models\LessonPostModel;
5 use LearnPress\Models\UserItems\UserCourseModel;
6 use LearnPress\Models\UserItems\UserLessonModel;
7 use LearnPress\Models\UserModel;
8
9 class LP_Jwt_Lessons_V1_Controller extends LP_REST_Jwt_Posts_Controller {
10 protected $namespace = 'learnpress/v1';
11
12 protected $rest_base = 'lessons';
13
14 protected $post_type = LP_LESSON_CPT;
15
16 protected $hierarchical = true;
17
18 public function register_routes() {
19 register_rest_route(
20 $this->namespace,
21 '/' . $this->rest_base,
22 array(
23 array(
24 'methods' => WP_REST_Server::READABLE,
25 'callback' => array( $this, 'get_items' ),
26 'permission_callback' => array( $this, 'get_items_permissions_check' ),
27 'args' => $this->get_collection_params(),
28 ),
29 'schema' => array( $this, 'get_public_item_schema' ),
30 )
31 );
32
33 register_rest_route(
34 $this->namespace,
35 '/' . $this->rest_base . '/(?P<id>[\d]+)',
36 array(
37 'args' => array(
38 'id' => array(
39 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
40 'type' => 'integer',
41 ),
42 ),
43 array(
44 'methods' => WP_REST_Server::READABLE,
45 'callback' => array( $this, 'get_item' ),
46 'permission_callback' => array( $this, 'get_item_permissions_check' ),
47 'args' => array(
48 'context' => $this->get_context_param(
49 array(
50 'default' => 'view',
51 )
52 ),
53 ),
54 ),
55 'schema' => array( $this, 'get_public_item_schema' ),
56 )
57 );
58
59 register_rest_route(
60 $this->namespace,
61 '/' . $this->rest_base . '/finish',
62 array(
63 'args' => array(
64 'id' => array(
65 'description' => esc_html__( 'A unique identifier for the resource.', 'learnpress' ),
66 'type' => 'integer',
67 ),
68 ),
69 array(
70 'methods' => WP_REST_Server::CREATABLE,
71 'callback' => array( $this, 'finish_lesson' ),
72 'permission_callback' => array( $this, 'get_item_permissions_check' ),
73 'args' => array(
74 'context' => $this->get_context_param(
75 array(
76 'default' => 'edit',
77 )
78 ),
79 ),
80 ),
81 )
82 );
83 }
84
85 /**
86 * Allow guests/unauthenticated users to query the list endpoint (filtered by slug).
87 * Per-item visibility is still enforced by check_read_permission, which only
88 * allows preview items for guests.
89 */
90 public function get_items_permissions_check( $request ) {
91 return true;
92 }
93
94 /**
95 * Checks if a course can be read.
96 *
97 * Correctly handles courses with the inherit status.
98 *
99 * @author Nhamdv
100 *
101 * @return bool Whether the post can be read.
102 * */
103 public function check_read_permission( $post_id ) {
104 if ( empty( absint( $post_id ) ) ) {
105 return false;
106 }
107
108 $post = get_post( $post_id );
109
110 if ( ! $post ) {
111 return false;
112 }
113
114 if ( lp_rest_check_post_permissions( $this->post_type, 'read', $post_id ) ) {
115 return true;
116 }
117
118 $post_status_obj = get_post_status_object( $post->post_status );
119 if ( ! $post_status_obj || ! $post_status_obj->public ) {
120 return false;
121 }
122
123 // Allow guests and unenrolled users to read items marked as preview.
124 $course_id = $this->get_course_by_item_id( $post_id );
125 if ( $course_id ) {
126 $course = learn_press_get_course( $course_id );
127 if ( $course ) {
128 $item = $course->get_item( $post_id );
129 if ( $item && $item->is_preview() ) {
130 return true;
131 }
132 }
133 }
134
135 $user_id = get_current_user_id();
136
137 if ( ! $user_id ) {
138 return false;
139 }
140
141 $user = learn_press_get_user( $user_id );
142
143 if ( empty( $course_id ) ) {
144 return false;
145 }
146
147 $can_view_content_course = $user->can_view_content_course( $course_id );
148
149 $can_view_item = $user->can_view_item( $post_id, $can_view_content_course );
150
151 if ( ! $can_view_item->flag ) {
152 return false;
153 }
154
155 // Can we read the parent if we're inheriting?
156 if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
157 $parent = get_post( $post->post_parent );
158
159 if ( $parent ) {
160 return $this->check_read_permission( $parent );
161 }
162 }
163
164 return true;
165 }
166
167 protected function get_object( $lesson = 0 ) {
168 global $post;
169
170 if ( false === $lesson && isset( $post, $post->ID ) && LP_LESSON_CPT === get_post_type( $post->ID ) ) {
171 $id = absint( $post->ID );
172 } elseif ( is_numeric( $lesson ) ) {
173 $id = $lesson;
174 } elseif ( $lesson instanceof LP_Lesson ) {
175 $id = $lesson->get_id();
176 } elseif ( ! empty( $lesson->ID ) ) {
177 $id = $lesson->ID;
178 }
179
180 return LP_Course_Item::get_item( $id );
181 }
182
183 /**
184 * Get course ID by lesson ID assigned.
185 *
186 * @param [type] $item_id
187 * @return void
188 */
189 protected function get_course_by_item_id( $item_id ) {
190 static $output;
191
192 global $wpdb;
193
194 if ( empty( $item_id ) ) {
195 return false;
196 }
197
198 if ( ! isset( $output ) ) {
199 $output = $wpdb->get_var(
200 $wpdb->prepare(
201 "SELECT c.ID FROM {$wpdb->posts} c
202 INNER JOIN {$wpdb->learnpress_sections} s ON c.ID = s.section_course_id
203 INNER JOIN {$wpdb->learnpress_section_items} si ON si.section_id = s.section_id
204 WHERE si.item_id = %d ORDER BY si.section_id DESC LIMIT 1
205 ",
206 $item_id
207 )
208 );
209 }
210
211 if ( $output ) {
212 return absint( $output );
213 }
214
215 return false;
216 }
217
218 public function finish_lesson( $request ) {
219 $response = new LP_REST_Response();
220
221 try {
222 $id = isset( $request['id'] ) ? absint( $request['id'] ) : '';
223 if ( empty( $id ) ) {
224 throw new Exception( esc_html__( 'Error: No lesson available!.', 'learnpress' ) );
225 }
226
227 $lessonModel = LessonPostModel::find( $id, true );
228 if ( ! $lessonModel ) {
229 throw new Exception( esc_html__( 'Error: Lesson is invalid!', 'learnpress' ) );
230 }
231
232 $course_id = $this->get_course_by_item_id( $id );
233 if ( empty( $course_id ) ) {
234 throw new Exception( esc_html__( 'Error: This lesson is not assigned in the Course.', 'learnpress' ) );
235 }
236
237 $courseModel = CourseModel::find( $course_id, true );
238 $userModel = UserModel::find( get_current_user_id(), true );
239
240 if ( empty( $courseModel ) || empty( $userModel ) ) {
241 throw new Exception( esc_html__( 'Error: No Course or User available.', 'learnpress' ) );
242 }
243
244 $userLessonModel = UserLessonModel::find_user_item(
245 get_current_user_id(),
246 $id,
247 $lessonModel->post_type,
248 $course_id,
249 LP_COURSE_CPT,
250 true
251 );
252
253 // Auto-create user lesson item if user is enrolled but never opened the lesson.
254 if ( ! $userLessonModel instanceof UserLessonModel ) {
255 $userCourseModel = UserCourseModel::find( get_current_user_id(), $course_id, true );
256 if ( ! $userCourseModel instanceof UserCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) {
257 throw new Exception( __( 'You must enroll the course first.', 'learnpress' ) );
258 }
259
260 $userLessonModel = new UserLessonModel();
261 $userLessonModel->user_id = get_current_user_id();
262 $userLessonModel->item_id = $id;
263 $userLessonModel->item_type = $lessonModel->post_type;
264 $userLessonModel->ref_id = $course_id;
265 $userLessonModel->ref_type = LP_COURSE_CPT;
266 $userLessonModel->parent_id = $userCourseModel->get_user_item_id();
267 $userLessonModel->status = LP_ITEM_STARTED;
268 $userLessonModel->save();
269 } elseif ( empty( $userLessonModel->parent_id ) ) {
270 // Repair legacy rows that were created without parent_id — without it, the lesson
271 // won't be counted by UserCourseModel::count_items_completed().
272 $userCourseModel = UserCourseModel::find( get_current_user_id(), $course_id, true );
273 if ( $userCourseModel instanceof UserCourseModel ) {
274 $userLessonModel->parent_id = $userCourseModel->get_user_item_id();
275 $userLessonModel->save();
276 }
277 }
278
279 $userLessonModel->set_complete();
280
281 // Recalculate course progress and persist to learnpress_user_item_results.
282 $userCourseModel = $userLessonModel->get_user_course_model();
283 if ( $userCourseModel instanceof UserCourseModel ) {
284 $course_results = $userCourseModel->calculate_course_results( true );
285 LP_User_Items_Result_DB::instance()->update(
286 $userCourseModel->get_user_item_id(),
287 wp_json_encode( $course_results )
288 );
289
290 // Auto-finish the course when evaluation criteria are met.
291 $can_finish = $userCourseModel->can_finish();
292 if ( $can_finish === true && ! is_wp_error( $can_finish ) ) {
293 try {
294 $userCourseModel->set_finish();
295 } catch ( Throwable $finishError ) {
296 // Non-fatal — user can still click Finish manually.
297 error_log( __METHOD__ . ' auto-finish: ' . $finishError->getMessage() );
298 }
299 }
300 }
301
302 $response->status = 'success';
303 $response->message = esc_html__( 'Congrats! You have completed the lesson successfully', 'learnpress' );
304 } catch ( Throwable $th ) {
305 $response->message = $th->getMessage();
306 }
307
308 return rest_ensure_response( $response );
309 }
310
311 public function prepare_object_for_response( $object, $request ) {
312 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
313 $data = $this->get_lesson_data( $object, $context, $request );
314
315 $response = rest_ensure_response( $data );
316
317 return apply_filters( "lp_jwt_rest_prepare_{$this->post_type}_object", $response, $object, $request );
318 }
319
320 protected function get_lesson_data( $object, $context = 'view' ) {
321 $request = func_num_args() >= 2 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) );
322 $fields = $this->get_fields_for_response( $request );
323
324 $id = ! empty( $object->ID ) ? $object->ID : $object->get_id();
325 $post = get_post( $id );
326 $data = array();
327
328 $assigned = $this->get_assigned( $id );
329
330 $course_id = 0;
331 if ( ! empty( $assigned ) && method_exists( $object, 'set_course' ) ) {
332 $course_id = $assigned['course']['id'];
333 $object->set_course( $course_id );
334 }
335
336 $userLessonModel = UserLessonModel::find_user_item(
337 get_current_user_id(),
338 $id,
339 LP_LESSON_CPT,
340 $course_id,
341 LP_COURSE_CPT,
342 true
343 );
344 $end_time = '';
345
346 if ( $userLessonModel instanceof UserLessonModel ) {
347 $end_time = $userLessonModel->get_end_time();
348 }
349
350 foreach ( $fields as $field ) {
351 switch ( $field ) {
352 case 'id':
353 $data['id'] = $id;
354 break;
355 case 'name':
356 $data['name'] = $post->post_title;
357 break;
358 case 'slug':
359 $data['slug'] = $post->post_name;
360 break;
361 case 'permalink':
362 $data['permalink'] = $object->get_permalink();
363 break;
364 case 'date_created':
365 $data['date_created'] = lp_jwt_prepare_date_response( $post->post_date_gmt, $post->post_date );
366 break;
367 case 'date_created_gmt':
368 $data['date_created_gmt'] = lp_jwt_prepare_date_response( $post->post_date_gmt );
369 break;
370 case 'date_modified':
371 $data['date_modified'] = lp_jwt_prepare_date_response( $post->post_modified_gmt, $post->post_modified );
372 break;
373 case 'date_modified_gmt':
374 $data['date_modified_gmt'] = lp_jwt_prepare_date_response( $post->post_modified_gmt );
375 break;
376 case 'status':
377 $data['status'] = $post->post_status;
378 break;
379 case 'content':
380 $data['content'] = 'view' === $context ? apply_filters( 'the_content', $post->post_content ) : $post->post_content;
381 break;
382 case 'excerpt':
383 $data['excerpt'] = $post->post_excerpt;
384 break;
385 case 'can_finish_course':
386 $data['can_finish_course'] = $this->check_can_finish_course( $id );
387 break;
388 case 'duration':
389 $data['duration'] = learn_press_get_post_translated_duration( $id, esc_html__( 'Lifetime', 'learnpress' ) );
390 break;
391 case 'assigned':
392 $data['assigned'] = $assigned;
393 break;
394 case 'results':
395 $data['results'] = $this->get_lesson_results( $object );
396 break;
397 case 'video_intro':
398 $data['video_intro'] = $this->get_video_intro( $id );
399 break;
400 }
401 }
402
403 $data['end_time'] = $end_time;
404 $data['meta_data'] = $this->get_course_meta( $id );
405
406 return $data;
407 }
408
409 public function get_video_intro( $id ) {
410 $video_intro = get_post_meta( $id, '_lp_lesson_video_intro', true );
411
412 if ( ! $video_intro ) {
413 return '';
414 }
415
416 if ( wp_oembed_get( $video_intro ) ) {
417 return wp_oembed_get( $video_intro );
418 }
419
420 return do_shortcode( $video_intro );
421 }
422
423 public function check_can_finish_course( $id ) {
424 $user = learn_press_get_current_user();
425
426 if ( ! $user || ! $id ) {
427 return false;
428 }
429
430 $course_id = $this->get_course_by_item_id( $id );
431
432 if ( empty( $course_id ) ) {
433 return false;
434 }
435
436 $course = learn_press_get_course( $course_id );
437
438 if ( $user && $course ) {
439 $check = $user->can_show_finish_course_btn( $course );
440
441 if ( $check['status'] === 'success' ) {
442 return true;
443 }
444
445 return false;
446 }
447
448 return false;
449 }
450
451 public function get_lesson_results( $lesson ) {
452 global $wpdb;
453
454 $output = array();
455
456 $user_id = learn_press_get_current_user_id();
457
458 if ( ! $user_id || ! $lesson ) {
459 return $output;
460 }
461
462 $id = ! empty( $lesson->ID ) ? $lesson->ID : $lesson->get_id();
463
464 $course_id = $this->get_course_by_item_id( $id );
465
466 if ( empty( $course_id ) ) {
467 return $output;
468 }
469
470 $query = $wpdb->prepare( "SELECT status FROM {$wpdb->prefix}learnpress_user_items WHERE user_id=%d AND item_id=%d AND ref_id=%d", $user_id, $id, $course_id );
471
472 $status = $wpdb->get_var( $query );
473
474 $output['status'] = ! empty( $status ) ? $status : '';
475
476 return $output;
477 }
478
479 public function get_course_meta( $id ) {
480 if ( ! class_exists( 'LP_Meta_Box_Lesson' ) ) {
481 include_once LP_PLUGIN_PATH . 'inc/admin/views/meta-boxes/lesson/settings.php';
482 }
483
484 $metabox = new LP_Meta_Box_Lesson();
485
486 $output = array();
487 foreach ( $metabox->metabox( $id ) as $meta_key => $object ) {
488 if ( is_a( $object, 'LP_Meta_Box_Field' ) ) {
489 $object->id = $meta_key;
490 $output[ $meta_key ] = $object->meta_value( $id );
491 }
492 }
493
494 return $output;
495 }
496
497 public function get_assigned( $id ) {
498 $courses = learn_press_get_item_courses( $id );
499
500 $output = array();
501
502 if ( $courses ) {
503 foreach ( $courses as $course ) {
504 $output['course'] = array(
505 'id' => $course->ID,
506 'title' => $course->post_title,
507 'slug' => $course->post_name,
508 'content' => $course->post_content,
509 'author' => $course->post_author,
510 );
511 }
512 }
513
514 return $output;
515 }
516
517 public function get_item_schema() {
518 $schema = array(
519 '$schema' => 'http://json-schema.org/draft-04/schema#',
520 'title' => $this->post_type,
521 'type' => 'object',
522 'properties' => array(
523 'id' => array(
524 'description' => __( 'A unique identifier for the resource.', 'learnpress' ),
525 'type' => 'integer',
526 'context' => array( 'view', 'edit' ),
527 'readonly' => true,
528 ),
529 'name' => array(
530 'description' => __( 'Course name.', 'learnpress' ),
531 'type' => 'string',
532 'context' => array( 'view', 'edit' ),
533 ),
534 'slug' => array(
535 'description' => __( 'Course slug.', 'learnpress' ),
536 'type' => 'string',
537 'context' => array( 'view', 'edit' ),
538 ),
539 'permalink' => array(
540 'description' => __( 'Course URL.', 'learnpress' ),
541 'type' => 'string',
542 'format' => 'uri',
543 'context' => array( 'view', 'edit' ),
544 'readonly' => true,
545 ),
546 'date_created' => array(
547 'description' => __( "The date the Course was created, in the site's timezone.", 'learnpress' ),
548 'type' => 'date-time',
549 'context' => array( 'view', 'edit' ),
550 'readonly' => true,
551 ),
552 'date_created_gmt' => array(
553 'description' => __( 'The date the Course was created, as GMT.', 'learnpress' ),
554 'type' => 'date-time',
555 'context' => array( 'view', 'edit' ),
556 'readonly' => true,
557 ),
558 'date_modified' => array(
559 'description' => __( "The date the Course was last modified, in the site's timezone.", 'learnpress' ),
560 'type' => 'date-time',
561 'context' => array( 'view', 'edit' ),
562 'readonly' => true,
563 ),
564 'date_modified_gmt' => array(
565 'description' => __( 'The date the Course was last modified, as GMT.', 'learnpress' ),
566 'type' => 'date-time',
567 'context' => array( 'view', 'edit' ),
568 'readonly' => true,
569 ),
570 'status' => array(
571 'description' => __( 'Course status (post status).', 'learnpress' ),
572 'type' => 'string',
573 'default' => 'publish',
574 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
575 'context' => array( 'view', 'edit' ),
576 ),
577 'content' => array(
578 'description' => __( 'Course content.', 'learnpress' ),
579 'type' => 'string',
580 'context' => array( 'view', 'edit' ),
581 ),
582 'excerpt' => array(
583 'description' => __( 'Retrieves the course excerpt..', 'learnpress' ),
584 'type' => 'string',
585 'context' => array( 'view', 'edit' ),
586 ),
587 'can_finish_course' => array(
588 'description' => __( 'Can finish the course', 'learnpress' ),
589 'type' => 'boolean',
590 'context' => array( 'view' ),
591 'readonly' => true,
592 ),
593 'duration' => array(
594 'description' => __( 'Duration', 'learnpress' ),
595 'type' => 'string',
596 'context' => array( 'view' ),
597 ),
598 'assigned' => array(
599 'description' => __( 'Assigned.', 'learnpress' ),
600 'type' => 'array',
601 'context' => array( 'view', 'edit' ),
602 'items' => array(
603 'id' => array(
604 'description' => __( 'Item ID.', 'learnpress' ),
605 'type' => 'integer',
606 'context' => array( 'view', 'edit' ),
607 ),
608 'title' => array(
609 'description' => __( 'Title.', 'learnpress' ),
610 'type' => 'string',
611 'context' => array( 'view', 'edit' ),
612 ),
613 'slug' => array(
614 'description' => __( 'Item slug.', 'learnpress' ),
615 'type' => 'string',
616 'context' => array( 'view', 'edit' ),
617 ),
618 'content' => array(
619 'description' => __( 'Item Content.', 'learnpress' ),
620 'type' => 'string',
621 'context' => array( 'view', 'edit' ),
622 ),
623 'author' => array(
624 'description' => __( 'Item Author.', 'learnpress' ),
625 'type' => 'integer',
626 'context' => array( 'view', 'edit' ),
627 ),
628 ),
629 ),
630 'results' => array(
631 'description' => __( 'Retrieves the Lesson result..', 'learnpress' ),
632 'type' => 'array',
633 'context' => array( 'view', 'edit' ),
634 'items' => array(
635 'status' => array(
636 'description' => __( 'Status.', 'learnpress' ),
637 'type' => 'string',
638 'context' => array( 'view', 'edit' ),
639 ),
640 ),
641 ),
642 'video_intro' => array(
643 'description' => __( 'Video intro.', 'learnpress' ),
644 'type' => 'string',
645 'context' => array( 'view', 'edit' ),
646 ),
647 ),
648 );
649
650 return $this->add_additional_fields_schema( $schema );
651 }
652
653 /**
654 * API get item lesson
655 * @since 4.1.6.9.1
656 *
657 * @param $request
658 *
659 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
660 * @throws Exception
661 */
662 public function get_item( $request ) {
663 $object = $this->get_object( (int) $request['id'] );
664
665 if ( ! $object || 0 === $object->get_id() ) {
666 return new WP_Error( "lp_rest_{$this->post_type}_invalid_id", esc_html__( 'Invalid ID.', 'learnpress' ), array( 'status' => 404 ) );
667 }
668
669 $item_id = (int) $request['id'];
670
671 $course_id = $this->get_course_by_item_id( $item_id );
672 $course = learn_press_get_course( $course_id );
673 if ( $course && is_user_logged_in() ) {
674 $user = learn_press_get_current_user();
675 $course_data = $user->get_course_data( $course_id );
676
677 if ( $course_data && $course_data->is_enrolled() ) {
678 $item = $course_data->get_item( $item_id );
679
680 if ( ! $item ) {
681 $item = LP_User_Item::get_item_object( $item_id );
682
683 $item->set_ref_id( $course_id );
684 $item->set_parent_id( $course_data->get_user_item_id() );
685 $item->update();
686 }
687 }
688 }
689
690 $data = $this->prepare_object_for_response( $object, $request );
691 $response = rest_ensure_response( $data );
692
693 if ( $this->public ) {
694 $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) );
695 }
696
697 return $response;
698 }
699 }
700