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